반응형

개요

  • AutoScaling 동작구조 비교
  • Node Scaler
  • Pod Scaler


요즘 EKS를 공부하고 있는데 와... addons가 너무많다

진짜 그냥 addons 조합인 것 같다.

 

진짜 추후에는 K8S를 효과적으로 사용하려면 addons 버전도 리스팅해서 사용해야 할 판이다.

하여튼, AutoScaling 구조를 한번 파악해보자...

 

K8S 에서는 주로 Node / Pod 의 대한 스케일 정책이 존재한다. 그냥 간단히 정리하면...

  • Node === EC2
  • Pod === Container 

용량이 급증하면 한개의 Deployment는 여러개의 Replica ( Pod ) 를 가지게 되고 -> Pod Scaler

한 Node안에 Pod를 더이상 배치할 수 없다면? -> 노드를 증가시켜 증가된 노드에 파드를 배치한댜 -> Node Sclaer

 

그럼 뭐가 이렇게 많은건데...

 

Node Scaler

Cluster AutoScaler

CA는 의외로 간단하다.

 

api-server 간의 계속 적인 통신을 진행한다.

  • pod가 pending 상태가 아닌지?
  • node의 사용량이 적절한지?
  • nodegroup -> 각각의 pod가 어디 소속인지?

그 과정에서 이벤트가 발생하게 되면, ASG에게 요청을 한다

  • desired +1 
  • desired -1

ASG가 대신 노드 (EC2)를 스케일아웃 , 스케일인 진행을 해준다.

 

Scale in 진행시에는 어떻게 될까?

  • CA는 각 Node를 주기적으로 점검
  • 유후노드를 발견하면 다른 Pod들을 선정된 노드로 옮길 수 있는가 평가
  • 대신... 아래 Pod가 있다면 해당 노드는 제거 불가
    • DeamonSet
    • Larget Storage
    • Static Pod
    • Unreplicated Pod
    • PodDisruptionBudget 위반

결국, CA는 ASG를 통해서 AutoScaling을 한다.

그리고 ASG는 알다시피 느리다...

Karpenter

카펜터도 일단 api-server와의 통신을 계속 유지한다.

  • Pending 파드 감지
  • 요구 리소스 분석
  • 인스턴스 타입 결정 -> 가장 적절한 인스턴스 타입을 스스로 선택 (여기서 비용절감이 이뤄짐)
  • EC2 인스턴스 Launch
  • 노드 Join 감시
  • 유후 시 EC2 Terminate

그 이후에 작업은 ASG가 아닌 본인이 직접 AWS EC2 API를 활용해서 호출한다

AWS EC2 API 는 ASG보다 더 빠르게 동작한다.

 

어떤게 더 좋은가?

당연히, Karpenter

  • 적절한 인스턴스를 선택한다는 점에서 -> 비용 효율적
  • OnDemand 뿐만 아닌, SPOT 인스턴스로 고려한다는 점
  • AWS API를 사용한 방식으로 인해, ASG보다 몇배 빠르다는 점

어떻게 사용해야 할까? (이거 관련해서도 Poc 해봐야 할듯)

  • 스팟인스턴스를 우선 사용하되, 없으면 온디맨드를 사용하게 끔 구성 (어차피 AutoScaling 이라 안정되면 스케일인 될거기에)
  • 특정 워크로드만 GPU , ARM 인스턴스로 보내도록 제어가능
  • TTLSecondsAfterEmpty 활용해서 노드 자동 회수 기능
  • taints 와 labels을 사용해서 워크로드 분리

 

Pod Scaler

 

HPA (Horizontal) + metrics server 꼭 필요함

HPA는 굉장히 간단하다. CPU / Memory 또는 Custom 한 메트릭을 기반으로 파드의 개수를 조절한다

보통 Helm 으로 Deployment를 구성하면 쉽게 사용이 가능하다

 

  • HPA는 주기적으로 Metric API에서 리소스 사용률을 확인
  • TargetValue와 현재 리소스 사용률을 비교해서 적절한 Replica 수를 조정
  • Deployment에 Patch를 요청해서 Replica 수를 조정
  • Scheduler가 새로운 파드를 스케쥴링

스케일 계산공식은 아래와 같다

desiredReplicas = ceil[
  currentReplicas × ( currentMetricValue / targetMetricValue )
]

예를들면,

현재 3개의 파드가 존재하고
CPU 사용률 평균이 80%, 목표가 50% 라면

3 * ( 80 / 50 ) = 4.8 -> 5개로 늘리는건다.

그럼...

현재 Pod가 1개고
CPU 사용률 평균이 90%고, 목표가 40% 라면

3 * ( 90 / 40 ) = 6.75 -> 7개

 

VPA (Vertical) -> 개발초기 때나 한번 해보려나? / 배치작업?

VPA는 파드의 실제 리소스 사용량을 모니터링 하고, 

리소스 요청값과 제한값에 의해 동적으로 조정하는 컨트롤러 <- 이게 무슨 얘기야...

 

아... 그러니까 VPA는 기본적으로 Deployment에 기재된 request, limit 을 기준으로

데이터를 분석해서, 자동으로 request 와 limit을 조절해준다... (재배포)

 

약간, 아래와 같은 형태라면 VPA를 사용해봄직 하다

  • 아직 서비스의 알맞은 리소스 값을 모를 때 (cpu, mem)
  • 리소스 사용량이 점진적으로 변하는 서비스일때 (데이터 처리 배치작업 ...)
  • 수평확장이 안되는 경우

 

KEDA (Event Driven 가능)

사실 이걸 제일 공부해보고 싶다

외부 이벤트에 상태를 감지해서 파드 수를 동적으로 스케일링 하는 Auto Sclaer 

 

  • 기본적으로 Keda Operator 를 활용해서 다른 외부시스템과 연동한다
  • 이벤트 상태를 주기적으로 체크
  • 이벤트 수량이 임계값을 초과한다면 -> HPA Replica 수 조절을 요청한다

아... HPA랑 같이 사용하는거군요...

 

만약... 구성하게 된다면

  • NodeScaler 는 무조건 Karpetner
  • PodScaler는 모두 사용할 것 같음
    • 간단한 CPU / Memory 스케일링은 HPA를 활용
    • VPA를 사용해서 최적값을 찾아나가게끔 모니터링 진행 (updateMode : off)
    • SQS, Kafka 와 같은 메시지 큐 부하에 의해 자체적인 스케일링을 하기위해 KEDA 설정
반응형

'Architecture > K8S' 카테고리의 다른 글

K8S - 시작 ( 용량산정 / 고려사항 )  (2) 2025.06.02
K8s - Karpenter  (2) 2025.06.01
반응형

개요

  • 바람직한 개인 / 조직간의 IaC 구조

 

이제 Terraform, Terragrunt, CDK 같은 Tool도 손에 익었고...

Atlassian, Terraform Cloud, Dagger 같은 오픈소스도 사용하고 왜 필요한지도 알았고...

소규모 조직 ~ 어느정도 중규모 조직에서도 IaC를 구성해봤고...

 

어떻게 하면 IaC 라는 하나의 작업을 대규모 개발조직에서 효과적으로 할 수 있을까?

 

IaC를 잘못사용하고 있다는 증거

꽤 많은 소스코드와 ,여러사람들과 협업을 하다보니 편하려고 구성한 Iac가 오히려 독이 되는 케이스가 존재했다.

내가 생각했을때 아래의 대한 내용이 포함된다면 그런 것 같다. (굉장히 주관적)

 

  • Module 을 중앙집중화 해서 사용하는가? 
  • Module 이 계층으로 구성되어 있는가? 
  • 서비스를 만들때, Terraform Code가 얼마나 증가하는가? 
  • 태그 같은 공통 규칙을 한눈에 볼수 있는가? (IaC 안에서...)  - 구조의 문제
  • Terraform 프로젝트의 Folder Depth가 3~4 번 이상 진행되는가? - 구조의 문제
  • Devops 인원만 Terraoform을 사용하는가? 
  • ...

 

Module의 대한 이야기

모듈을 사용하는 이유는 뭐.. 너무 명확하다. 

전사내의 인프라 규칙자체를 중앙화 하는 목적이기도 하고, IaC 코드내에서도 어느정도 통일이 되게끔 하는 이유도 있다. (그 외에도 많음)

 

하지만 이 논리가 무너지는 순간이 있다. 이로인해 모든 모듈 구성이 망가진다.

  • 모듈을 사용하되, 다양한 변칙이 존재하는 경우
  • 모듈이 모듈을 감싸고 있는 경우 ( 상위모듈 = 하위모듈 + 하위모듈 )
  • 바쁜 경우 (이건 뭐.. 어쩔 수 없음)

 

예를 들어보자...

 

사실 하나의 모듈이 모든 의견을 수렴해서 짜는 건 당연하다

하지만 IaC를 잘 다루지 못하거나, Terraform Module 구성하는 것에 큰 경험이 없다면

무너지기 마련이다. -> 그냥 이순간 부터는 LB 관련된 모듈이 다양하게 쏟아진다. (재앙)

 

그럼 모듈은 어느 방식으로 구성하면 좋은가의 대한 얘기를 해보자...

보통 Back 개발을 하다보면 Layer를 주로 나누게 된다. Layer 구간에 각각의 목적성에 맞춘 코드를 작성한다

그것처럼 IaC도 결국 계층별로 Module을 정의하면 위 문제에서 나왔던 상위모듈 / 하위모듈 문제도 어느정도 해결 될 수 있다.

IaC Layer Architecture

모듈자체를 어느정도 계층을 두면 GraceFully 하게 해결될 수 있다.

Resource 라는 작은 모듈을 만들고 거기서 필요한 Set를 Service Layer로 구성하고,

Service 의 조합이 구성되면 하나의 Platform 모듈을 구성하게 된다면 -> 흔히 말하는 인프라를 찍어내는 형태로도 활용이 가능하다

 

위 구조형태로 구성하게 된다면, 문제로 제기되었던 아래문제도 어느정도 해결 될 수 있다.

  • 서비스를 만들때, Terraform Code가 얼마나 증가하는가?

어느 회사에서는 인프라 구축의 대한 요청이 들어오면, 

하나하나 만든다고 한다

  • ALB
  • ECS
  • AutoScaling Group
  • S3
  • Athena
  • Route53 Record
  • ...

그리고 다 하나씩 구성하면 apply 하면서 ... -> 정말 비효율적이다

 

회사내에서는 어느정도 인프라의 규격이 정해지기 마련이다.

해당 구조의 대한 Service Layer, Platform Layer에 모듈로만 정의해놓으면 "딸깍" 이면 끝날 수 도 있는 업무다.

 

 

Terraform을 사용하는 인원은 누구인가?

이건 회사마다 다를텐데... 거의 인프라를 담당하는 조직에서만 사용할 것 이다.

내가 생각했을때는 이건... 좋지 않다 왜 좋지 않냐? 

 

보통의 조직에서는 개발자 n 명 당, 인프라 인원 m 명을 배정해서 업무를 진행한다. (어딜가도 인프라 인원이 많은데는 본적이 없음)

  • 개발자 >>>>>> 인프라

그 과정에서 인프라 개발자들은 보통 아래 문제의 대해서 고민한다.

  • Compliacne 수립
  • 인프라 자동화 / 인프라 구성 일손 덜기
  • ...

그 과정에서 어느정도는 IaC 의 대한 일손을 개발자들에게 줘도 되지 않을까?

왜냐? 개발자들이 안바빠서? No

경험상, 위와같은 여러가지 상황이 많이 발생한다.

그럼... 간단하게 생각해 봤을때는?

 

  • 인프라를 모르는 사람은 -> Devops 가 짜주면 됨 -> 근데 바쁨 (다른 업무) -> 나중에 함
  • 인프라를 어느정도 아는사람은 -> Devops 가 구성 규칙만 알려주면 됨 -> 그럼 알아서 짬
  • 인프라를 남몰래 구축하는 사람은 -> 구축하는 Platform 이나 통로를 하나만 구성해주면 됨 -> 그럼 알아서 짬

 

자, 그럼 개발자분들이 어떻게 코드를 구성하게 할까? 간단한다

IaC 구조에서 Presentation Layer를 하나도 고려해본다면 그렇게 어렵진않다.

 

 

결국 하나의 통로만 만들어주면 된다.

AWS 의 Proton 이나 Catalog 와 같은 형태를 만들어주고,

그것이 json, yaml 로만 구성할 수 있다면 Terraform 에서는 jsondecode, yamldecode 메서드를 통해서 변수를 참조할 수 있다.

 

부작용

물론 위 구조가 정답일까? No

아마 반발이 많을 것 이다.

 

 

다양한 조직에는 다양한 사람이 있는 만큼 "왜 이걸 우리가 하냐" 라고 하는 사람도 있을 것이고,

"이럴거면 왜 Devops 가 필요하나" 여러가지 이유가 있을 수 있다.

 

또한 위 구조를 만드는 것 자체가 팀내에서는 큰 Challenge 가 될수도 있다.

 

결론...

저런 플랫폼을 만들어 보고싶음.. (희망) 

Devops 는 문화를 만드는 조직이니 만큼, IaC 라는 Tool에 국한되지 않고 더 큰 방법으로 활용하게 하고싶음

 

반응형
반응형

이제... 드디어 K8S 를 실무에서 사용해볼 수 있는 기회가 왔다.

이제 앞으로 나도 왜 K8S를 사용하는지 장점이 뭔지, 단점이 뭔지 더 디테일하게 토론할 수 있을것이다.

 

하지만, EKS를 구성하기전에 몇몇 고려사항등을 검토해봐야 한다.

 

용량 산정

집(용량) 구하기 Let'go

 

 

기본적으로 K8S는 Cluster 구조로 이루어져 있고, 1개의 Master Node와 여러개의 Worker Node로 이루어져 있다.

우리는 Pod라는 서비르를 Worker Node에 띄우지만, 이 구성을 어떤 방식으로 할 것이고 Node 의 용량을 어떻게 산정해야 할 것인가는 

중요한 문제이다.

 

용량을 산정하지 못하면, Pod는 Pending 상태로 계속 유지될 것이고 서비스는 정상적으로 동작하지 못할 것이다.

 

만약 Cluster 총 용량에 따라서 Node를 작게, 크게 나눈다라면...

클러스터 용량 작은 노드 여러개 큰 노드 여러개
8 CPU , 16 GB Memory - 2 core 4 GB Memory 
- 총 4 노드
- 8 core 16 GB Memory
- 총 1 노드
400 CPU, 800 GB Memory - 2 core 4 GB Memory
- 총 200 노드
- 8 core 16 GB Memory
- 총 50 노드

 

각각의 장단점은 뭘까?

  장점 단점
작은 노드 여러개 - 노드 장애 시, 클러스터 전체에서 피해가 적다 (노드 여러개니까...)
- HA 구성 시 유리
- 관리하는 노드가 많아 -> 마스터 노드 부하증가
- 공식적으로 5000개의 이하 권장 ( 500개정도여도 부하가 생김) -> Master Node 스펙업하면 됨
- 노드마다 기본적으로 사용되는 리소스 존재

리소스 스펙이 낮다면 (전체 리소스 대비 실제로 사용하는 서비스 리소스가 적을 수 있음)
- Karpenter...
- Keda...
- DeamonSet...
큰 노드 여러개 - 관리의 편의성
- 장비의 대한 업데이트 / 트러블슈팅이 적다
- 총 예산이 저렴하다 ( 10 > 1 )
- 큰 어플리케이션을 돌릴 때 유용
- 하나의 노드에 많은 pod가 실행된다 
- Kubelet, Container Runtime에 부하
- 노드 당, 최대 110개의 pod를 추천

- Rplicaiton 효율이 떨어짐 (HA 의미가 없음)
- 장애발생 시 위험함 (다른 노드에 부하가 클 수 있음)

 

결론적으로

 

큰 노드 여러개는

  • 관리가 편하고, 장비 및 업데이트의 대한 트러블 슈팅이 적고, 큰 어플리케이션을 돌릴때 유용하나...
  • 하나의 노드에 많은 Application이 동작하는 만큼 부하 및 서비스 장애 시, 다른 노드 즉 Cluster 전체의 대한 장애가 발생할 수 있다

작은 노드 여러개는

  • 장애발생 시, 클러스터 전체에 피해가 적을 수 있고 HA 구성할때 유리하다... -> 작은 서비스가 여러개라면 최적임
  • Node가 많아지는건 Master Node에 대해 부하가 발생할 수 있으며
  • 기본적으로 Node 마다 구성되는 리소스들이 존재한다 (기본 구성의 Spec을 미리 알아야 할듯하다)
    • Karpenter
    • Keda
    • CSI
    • Kube Proxy...
    • DeamonSet
  • 이러한 리소스들이 어느정도 잡아먹는데, Node 스펙이 낮다면 Application이 얼마 못들어감

 

좋아.. 그럼 모든걸 고려했다면 아래 사이트에 이동해서 

결과적으로 내가 만든 서비스가 어떤 K8S Node Type이 적당한지 계산을 해보자...

https://learnk8s.io/kubernetes-instance-calculator

 

Kubernetes instance calculator

Explore the best instance types for your Kubernetes cluster interactively.

learnk8s.io

 

 

고려사항

  • 스토리지 및 네트워크 연동
  • 쿠버네티스 업그레이드 및 패치
  • 모니터링 및 로깅환경 구축
  • 마스터노드에 대한 장애복구

 

클러스터 모니터링

클러스터 로깅

 

아티펙트 저장소

  • 기본적으로 Docker Hub나 외부 Helm 저장소를 사용하나,
  • 외부 저장소에 오류로 인해서 실제 Application에 문제가 발생할 여지가 존재하기에 -> 내부 저장소를 사용하자

CI/CD 파이프라인

반응형

'Architecture > K8S' 카테고리의 다른 글

k8s AutoScaling 동작구조 비교  (0) 2025.06.21
K8s - Karpenter  (2) 2025.06.01
반응형

개요

  • K8S AutoScaling
  • AutoScaler vs Karpenter
  • Karpenter 동작원리
  • Karpenter Terraform

K8S AutoScaling

K8S 는 기본적으로 Node / Pod 기반으로 서비스가 동작한다

여기서의 Node는 EC2, Pod 는 Container로 해당된다.

 

이때, Node 즉 EC2의 경우 서비스가 많아짐에 따라 EC2 AutoScaling을 진행하게 된다.

  1. Pod AutoScaling 구성 
  2. Worker Node에 Pod가 많아짐 
  3. 기존 Node 용량보다 Pod가 많아질 경우, EC2 Node 가 더 생김

그럼 그냥 EC2 AutoScaling 을 Default 하게 사용하면 안되나? 

그렇게 써도 되긴하는데, 좀더 효율적인 방법의 AutoScaling 방법을 사용해야 한다.

 

EC2 Launch Template vs AutoScaler vs Karpenter

 

EC2 Launch Template + ASG

EC2 Launch Template + ASG

가장 기본적인 방식이다.

EC2 인스턴스 그룹을 미리 정의해두고, 트래픽에 따라 인스턴스를 수동 / 자동으로 확장할 수 있다.

Kubernetest 의 Pod 상태를 직접적으로 알기 어렵기 때문에 과잉 할당으로 인한 불필요한 비용이 발생할 여지가 존재함

운영자가 직접 Scale 정책을 설계해야 하기때문에, 유지관리 비용이 크다.

AutoScaler

AutoScaler

K8S 에서 주로 사용하는 AutoScaling 컴포넌트

Pod가 Node에 스케쥴링되지 못할 때 -> 필요에 따라 EC2를 자동으로 생성한다.

ASG완 연동되어 작동하며, 기본적으로 NodeGroup 단위로 동작한다.

다만, 아래와 같은 단점이 존재한다.

  • 각 NodeGroup의 대한 설정이 필요
  • 리소스 낭비가 발생하기 쉽다.
  • 스케일 속도가 느리다 ( AutoScaling 자체가 기본적으로 느림 )

Karpenter (밑에 사진 참조)

AWS 가 만든 오픈소스 Kubernetes AutoScaler (참 잘만듬)

Pod 스케쥴링 요청에 따라, 가장 적합한 인스턴스 타입과 크기를 실시간으로 계산하여 자동 생성한다

NodeGroup이 아닌, 단일 프로비저닝 정책만으로도 다양한 EC2 타입을 활용할 수 있다. (SPOT도 가능)

프로비저닝 속도가 빠르다 (awscli 사용해서 EC2를 생성하기 때문에 AutoScaling 보다 빠르다)

 

그냥 결론적으로 보면... (주관적)

  • 빠른가? -> Karpenter >>>>>> Launch Template == AutoScaler
  • 비용최적화로 EC2를 설정하는가? -> Karpenter 
  • 설정이 어렵나? -> Karpenter == Launch Template == AutoScaler

 

Karpenter 동작원리

 

pod 생성 후 -> node attach

 

Academic 한 이론으로는 아래와 같다.

  1. Pending Pod를 감지한다 -> Kubernetes에 Scheduling Cycle 중 스케쥴되지 못한 Pod를 감지한다
  2. Pod의 요구 리소스를 분석한다 (이 Pod를 수용할 수 있는 최적의 인스턴스를 구하는 단계)
    1. CPU / Memory / GPU 리소스 요구량
    2. NodeSelector, Trolerations, Affinity 제약 조건
    3. Storage / Zone / OS 특수 요구사항
  3. 적합한 인스턴스 타입 탐색
    1. 리전 및 가용역역
    2. on-Demand or SPOT 인스턴스 여부
  4. 인스턴스 프로비저닝
    1. 위 단계를 지나치고 선택된 EC2 타입을 직접 API로 생성 (아주 빠름)
      1. AMI
      2. Kubernetes Kubelet
      3. IAM Role, SG, Subnet 설정
  5. Pod 스케쥴링 및 Node 연결
    1. 생성된 EC2 인스턴스를 K8S Node로 Join
    2. Pending Pod를 해당 Node에 스케쥴링
  6. 유후 Node 감지 및 제거
    1. consolidation, ttlSecondsAfterEmpty, expiration 등의 설정으로 유휴 노드 제거 전략 조절 가능

 

하지만 난, 저걸 이해못함 -> 내가 이해한 바로는 ...

처음 카펜터를 구성하면 4가지? 를 생성 및 참조하게된다.

  1. 태그 참조
  2. Karpenter IRSA
  3. Karpenter Controller
  4. Karpenter Provisioner

태그참조

카펜터는 참 신기한 놈이다.

카펜터를 사용하겠다를 태그를 활용해서 구성한다.

  • 카펜터가 구성되는 EC2
  • 카펜터를 사용해서 Provisioning 되는 EC2의 SG
  • 카펜터를 사용해서 Provisioning 되는 EC2가 속한 Subnet 

에 올바른 Tag가 기입이 되어야 한다.

설정마다 다르겠지만, 나는 Blueprint

 

Karpenter IRSA

k8s에 장점이자 단점인, Service Account이다

즉, pod의 권한을 IAM Role로서 제한을 할 수 있다.

 

Node안에는 다양한 서비스의 Pod들이 떠있다. 그렇기 때문에 IRSA를 사용해서

namespace 별로, 각각 다른 serviceAccount (SA) 를 만들고 해당 Pod에 각각 다른 IAM Role을 부여해서 사용한다.

 

Karpenter Controller (언제 노드를 만들까?)

  • Deployment
  • 실제로 노드를 프로비저닝하는 실행 컴포넌트
  • 기본적으로 1개의 replica로 실행된다 (namespace = karpenter)

Karpenter Provisioner (어떤 노드를 만들까?)

  • Custom Resource (CR)
  • 노드 프로비저닝의 정책을 정의하는 설정 컴포넌트 <- 여기에 적힌 구성대로 Karpenter가 최적의 EC2를 찾아줌

 

Karpenter Terraform

variable "eks_attr" {
  default = {
    "name" : "donggyu-eks"
    "version" : "1.32"
  }
}

module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "~> 20.31"

  cluster_name                    = lookup(var.eks_attr, "name")
  cluster_version                 = lookup(var.eks_attr, "version")
  cluster_endpoint_public_access  = true // 외부에서 접근 가능
  cluster_endpoint_private_access = true // 내부에서 접근 가능

  create_cluster_security_group = false
  create_node_security_group    = false

  # Optional: Adds the current caller identity as an administrator via cluster access entry
  enable_cluster_creator_admin_permissions = true

  vpc_id     = local.vpc.vpc_id
  subnet_ids = values(local.vpc.was_subnets)

  # EKS Addons
  cluster_addons = {
    coredns = {
      most_recent = true
    }
    kube-proxy = {
      most_recent = true
    }
    vpc-cni = {
      most_recent = true
    }
    aws-ebs-csi-driver = {
      most_recent = true
    }
    # eks-pod-identity-agent = {}
  }

  eks_managed_node_groups = {
    karpenter = {
      instance_types = ["t4g.medium"]
      ami_type = "AL2_ARM_64"
      min_size = 2
      max_size = 5
      desired_size = 2
      # capacity_type = "SPOT"

      iam_role_additional_policies = {
        "AmazonSSMManagedInstanceCore" = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
      }
    }
  }

  node_security_group_additional_rules = {
    ingress_karpenter_webhook_tcp = {
      description                   = "Control plane invoke Karpenter webhook"
      protocol                      = "tcp"
      from_port                     = 8443
      to_port                       = 8443
      type                          = "ingress"
      source_cluster_security_group = true ## 해당 규칙의 소스를 cluster가 가지고있는 보안그룹으로 지정 (node간 통신허용)
    }
  }


  tags = merge({
    Blueprint = lookup(var.eks_attr, "name")
    }, {
    "karpenter.sh/discovery" = lookup(var.eks_attr, "name")
  })
}

################################################################
######## karpenter
################################################################
resource "aws_iam_instance_profile" "karpenter" {
    name = "KarpenterNodeInstanceProfile-${module.eks.cluster_name}"
    role = module.eks.eks_managed_node_groups["karpenter"].iam_role_name
}



################################################################
######## aws-auth
################################################################
module "eks_aws_auth" {
  source  = "terraform-aws-modules/eks/aws//modules/aws-auth"
  version = "~> 20.0"

  manage_aws_auth_configmap = true

  aws_auth_roles = [
    {
      rolearn  = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/AWSReservedSSO_AdministratorAccess_18f0ecd34fab4ea6"
      username = "admin"
      groups   = ["system:masters"]
    },
    {
      rolearn = module.eks.eks_managed_node_groups["karpenter"].iam_role_arn
      username = "system:node:{{EC2PrivateDNSName}}"
      groups   = ["system:nodes", "system:bootstrappers"]
    }
  ]

  aws_auth_users = [
    {
      userarn  = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:user/admin"
      username = "admin"
      groups   = ["system:masters"]
    }
  ]
}
## karpenter pod가 AWS에 접글할 수 있도록 IRSA를 생성
module "karpenter_irsa" {
  source  = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks"
  version = "~> 4.21.1"

  role_name                          = "karpenter-controller-${module.eks.cluster_name}"
  attach_karpenter_controller_policy = true

  karpenter_controller_cluster_id = module.eks.cluster_arn
  karpenter_controller_node_iam_role_arns = [
    module.eks.eks_managed_node_groups["karpenter"].iam_role_arn
  ]

  karpenter_controller_ssm_parameter_arns = [
    "arn:aws:ssm:*:*:parameter/aws/service/eks/optimized-ami/*"
  ]

  oidc_providers = {
    ex = {
      provider_arn               = module.eks.oidc_provider_arn
      namespace_service_accounts = ["karpenter:karpenter"]
    }
  }
}

# EKS 클러스터 접근 권한을 위한 IAM 정책
resource "aws_iam_policy" "karpenter_eks_access" {
  name        = "karpenter-eks-access-${module.eks.cluster_name}"
  description = "Policy for Karpenter to access EKS cluster"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "eks:DescribeCluster",
          "eks:ListClusters",
        ]
        Resource = module.eks.cluster_arn
      },
      {
        Effect = "Allow",
        "Action" : [
            "ec2:RunInstances",
            "ec2:TerminateInstances",
            "ec2:DescribeSpotPriceHistory",
            "pricing:GetProducts"
        ],
        "Resource" : "*"
      }
    ]
  })
}

# IAM 정책을 Karpenter IRSA 역할에 연결
resource "aws_iam_role_policy_attachment" "karpenter_eks_access" {
  role       = module.karpenter_irsa.iam_role_name
  policy_arn = aws_iam_policy.karpenter_eks_access.arn
}

# karpenter_helm.tf

resource "helm_release" "karpenter" {
  name       = "karpenter"
  repository = "oci://public.ecr.aws/karpenter"
  chart      = "karpenter"
  version    = "v0.27.0"
  namespace  = "karpenter"
  create_namespace = true

  set {
    name  = "serviceAccount.annotations.eks\\.amazonaws\\.com/role-arn"
    value = module.karpenter_irsa.iam_role_arn
  }

  set {
    name  = "settings.aws.clusterName"
    value = module.eks.cluster_name
  }

  set {
    name  = "settings.aws.defaultInstanceProfile"
    value = "KarpenterNodeInstanceProfile-${module.eks.cluster_name}"
  }

  set {
    name  = "settings.aws.interruptionQueueName"
    value = module.eks.cluster_name
  }

  wait = true
}

# karpenter_manifest.tf
resource "kubectl_manifest" "karpenter_node_template" {
  yaml_body = file("${path.module}/karpenter.yaml")

  depends_on = [helm_release.karpenter]
}
apiVersion: karpenter.sh/v1alpha5
kind: Provisioner
metadata:
  name: provisioner
spec:
  requirements:
    - key: "node.kubernetes.io/instance-type"
      operator: In
      values: [ "t4g.medium" ]
    - key: "topology.kubernetes.io/zone"
      operator: In
      values: [ "ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c" ]
    - key: "eks.amazonaws.com/capacityType"
      operator: In
      values: [ "ON_DEMAND" ]
    - key: "kubernetes.io/arch"
      operator: In
      values: [ "arm64" ]
  
  # 생성할 인스턴스의 최대 리소스
  limits:
    resources:
      cpu: "10"
      memory: 20Gi
      # nvidia.com/gpu: 16
  
  # karpenter 프로비저닝 메커니즘 사용
  # ttlSecondsAfterEmpty 와 consolidation 은 동시에 사용 불가
  # consolidation:
  #   enabled: true
  #   ttlSecondsUntilExpired: 2592000 # 30 Days = 60 * 60 * 24 * 30 Seconds;      
  ttlSecondsAfterEmpty: 30

  # 생성된 인스턴스(worker node)에 지정되는 label
  labels:
    environment: donggyu-eks
    managed-by: karpenter
    Blueprint: donggyu-eks

  # 생성된 인스턴스(worker node)에 지정되는 taints
  # taints:
  #   - key: nvidia.com/gpu
  #     value: "true"
  #     effect: NoSchedule

  provider:
    # 생성한 인스턴스에 어느 보안 그룹을 적용할 것인지 보안 그룹의 태그로 지정
    securityGroupSelector:
      Blueprint: donggyu-eks
    
    # 어느 서브넷에 인스턴스를 생성할 것인지 태그로 지정
    subnetSelector:
      environment: donggyu-eks
      managed-by: karpenter
    
    # 생성된 인스턴스의 태그를 지정  
    tags:
      Blueprint: donggyu-eks

 

 

반응형

'Architecture > K8S' 카테고리의 다른 글

k8s AutoScaling 동작구조 비교  (0) 2025.06.21
K8S - 시작 ( 용량산정 / 고려사항 )  (2) 2025.06.02
반응형

개요

  • ECS 이전할때 Trouble Shooting...

 

ECS 이전할때 Trouble Shooting...

3주정도 ECS 이전하면서 

고생을 너무 많이 했다. 뒤돌아보면 별거 아닌 이슈들도 많았지만...

NEXT ( SSR ) 을 어느정도 공부할 수 있었던 계기가 된것같다. 

 

하지만 Front 는 여전히 너무 어려운것 같다...

 

1. Dockerizing 

Vercel 이라는 플랫폼이 동작하는 원리를 보면 아래와 같다.

  1. 브랜치 배포
  2. 배포된 브랜치 checkout 후, 그대로 build ( npm run build )
  3. build 된 산출물 기반으로 서버 시작 ( npm run start )

약간 과장된 부분이 없지 않아 있지만, 그냥 EC2에 올려서 쓰는느낌이다.

그래서 처음부터 Dockerzing 을 진행해야 했다.

 

여기서 살짝 정신이 1차로 나간것 같다.

// monorepo
################################################ Base 
FROM node:20-bullseye-slim AS base
## add canvas lib
RUN apt-get update && apt-get install -y \
    tree \ 
    python3 \
    make \
    g++ \
    build-essential \
    libcairo2-dev \
    libjpeg-dev \
    libpango1.0-dev \
    libgif-dev \
    librsvg2-dev \
    && rm -rf /var/lib/apt/lists/*
RUN npm i -g pnpm
################################################ Install 
FROM base AS builder
WORKDIR /usr/src/app

COPY package*.json pnpm-lock.yaml pnpm-workspace.yaml turbo.json .npmrc  ./
COPY apps/refund-web ./apps/refund-web

## 현재 작업하는 폴더 내 .env를 위치시켜야 함
## build 시 .env 가 알아서 .next/standalone으로 위치함
COPY .env ./apps/refund-web/.env
COPY packages ./packages
RUN pnpm install
ENV NEXT_PUBLIC_CDN_BASE_URL=[CDN 주소]
ENV NEXT_PUBLIC_APP_NAME=[APP NAME]
ENV NEXT_PUBLIC_RESOURCE_CENTER_URL=[CDN 주소]
ENV NEXT_PUBLIC_ZENV=[environment]
ENV NODE_ENV=[environemnt]

RUN pnpm run build:refund
RUN pnpm prune --prod
################################################ Runner
FROM node:20-bullseye-slim AS runner

COPY --from=builder /usr/src/app/apps/refund-web/.next/standalone/ .
COPY --from=builder /usr/src/app/apps/refund-web/.next/static ./.next/static
COPY --from=builder /usr/src/app/apps/refund-web/public ./apps/refund-web/public
EXPOSE 3000
ENV HOSTNAME=0.0.0.0

 

// single repo
FROM node:20.18.3-bullseye-slim AS base
RUN apt-get update && apt-get install -y \
    tree \
    python3 \
    make \
    g++ \
    && rm -rf /var/lib/apt/lists/*
WORKDIR /usr/src/app
COPY package*.json pnpm-lock.yaml ./
RUN npm i -g pnpm
# RUN pnpm install --frozen-lockfile --production
RUN pnpm install
RUN pnpm add sharp
COPY . .

##################### Requrie Changes... #####################
ENV CDN_BASE_URL=[CDN]
ENV NEXT_PUBLIC_SERVER_ZENT_API_URL=[API]
ENV APP_NAME=[APP_NAME]
ENV PORT=[PORT]
ENV NODE_ENV=[ENVIRONEMNT]
RUN pnpm run build
RUN rm -rf ./.next/cache

FROM node:20.18.3-bullseye-slim AS runner
WORKDIR /usr/src/app
COPY --from=base /usr/src/app/public ./public
COPY --from=base /usr/src/app/package*.json .
COPY --from=base /usr/src/app/.next/standalone ./
COPY --from=base /usr/src/app/next.config.js ./
COPY --from=base /usr/src/app/.next/static ./.next/static 
EXPOSE 3000
ENV HOSTNAME=0.0.0.0

 

자... Dockerfile 작성할때 유의할점도 같이 보자..

 

2. env 파일 생성

Backend 서버에서는 env 파일을 따로 두진 않고, ECS TaskDefinition에서 값을 넣어준다.

그렇게 해서 자체적으로 mapping 되게 하지만 NEXT는 달랐다.

 

애초에 Docker build 타임에 env 파일이 필요했다.

그래서 DockerBuild 하는 시점 전에, CD 쪽에서 .env 값을 만들어 줘야 했다.

COPY .env ./apps/refund-web/.env

 

3.  hostname 이슈 ( Next 13.4.13)

이건 특정 버전 이슈였는데, Next 13.4.13 버전에서 내부적으로 API를 호출하는데,

Docker환경에서만 비정상적인 내부포트를 자동으로 생성

그래서 해당 버전은 13.4.19 버전으로 수정했었다.

 

추후 이 문제로 많은 논의를 진행했는데 NEXT 자체가 버전문제가 많아서

최대한 15버전으로 옮기겠단 말을 해주셔따 (FE 감사)

 

 

4. next.config.js

NEXT 에서 Dockerzing 시, output 되는 dist 폴더 사이즈를 줄이기 위해 옵션을 추가해야한다. (output)

또한 cs / jss 접근 시, CDN 으로 접근하기 위해서도 아래 옵션을 기재해야 한다. (assetPrefix)

// next.config.ts
const { version: packageVersion } = require("./package.json");
/** @type {import('next').NextConfig} */
const zentNextConfig = {
  ...nextConfig,
  output: 'standalone', ## 이 옵션 필요
  assetPrefix: `${process.env.CDN_BASE_URL}/${process.env.APP_NAME}/${packageVersion}`, ## 이 옵션 필요 //.. options
  compiler: {
    //...nextConfig.compiler,
    removeConsole: {
      exclude: ["error"],
    },
  },
};
/** @type {import('next').NextConfig} */

 

5. health check route 추가

ECS로 옮기게 되면, Target Group이 Health Check를 통해서 api에게 질의를 한다.

이때 healthCheck 할 수 있는 router를 추가해야한다. (버전마다 상이)

  • next 12 
    • pages/api/ping.ts
  • next 13 ~ 15
    • src/app/ping/route.ts

6. hostname 이슈

몇몇 콘솔은 Cognito 를 사용하여 사용자 인증을 진행하였다.

이때, ALB -> ECS -> Cognito -> OKTA 형태로 동작하는데 아래와 같은 이슈가 발생하였다.

 

위 화면은 OKTA 인증 후 나타나면 화면이다.

.Har (관리자 콘솔 -> 네트워크 탭) 파일을 확인해도 별다른 내용을 확인 할 수 없었음 ...

 

ALB 내 Cognito 연결을 구성하는 부분이 있지만 -> 우리는 CallBack을 기반으로 동작해서 해당 문제는 아니었음

결국 Docker 내 hostname 이슈 였음 (아래와 같이 해결)

...
CMD HOSTNAME="https://[domain]" node ./apps/op/server.js

 

OKTA 에서 Service로 Redirect 해줄때, 내부 ip로 전달을 해주는 (hostname) 이슈였다. 

 

7. CD 구성... *****

#!/bin/bash

## if occured error > process.exit(1)
set -e 

# Variables
SERVICE_NAME=$1
ENV=$2
TAG=$3
S3_BUCKET=$4
DOCKERFILE_PAHT=$5
ASSET_PREFIX=$6

_ENV=$ENV

if [ "$ENV" == "prd" ]; then 
    echo "Deploy to production"
    ACCOUNT_ID=[account_id]
    ECS_CLUSTER_NAME=[prd-cluster]
    PORT="3000"
    CPU=2048
    MEM=4096
else 
    echo "Deploy to development"
    ACCOUNT_ID=[account_id]
    ECS_CLUSTER_NAME=[dev-cluster]
    PORT="3000"
    CPU=1024
    MEM=2048
fi

## 예외) env-1 ~ n
if [[ "$ENV" =~ ^([a-zA-Z]+)-([0-9]+)$ ]]; then
    ENV="${BASH_REMATCH[2]}-${BASH_REMATCH[1]}"
    ECR_REPOSITORY=dkr.ecr.ap-northeast-2.amazonaws.com/${SERVICE_NAME}-${BASH_REMATCH[2]}/${BASH_REMATCH[1]}
else
    ECR_REPOSITORY=dkr.ecr.ap-northeast-2.amazonaws.com/${SERVICE_NAME}/${ENV}
fi

echo $TAG
echo $ENV
echo $ECR_REPOSITORY

TASK_VERSION=v3.0.0                                    ## Task Definition Version
############################## Not Changed ##############################
TEAM=client
ECS_SERVICE_NAME=${SERVICE_NAME}-${ENV}-svc

ssm v4 --json ./devops-repo/task_definition/${TASK_VERSION}/task_def.json \
    --zent_account_id ${ACCOUNT_ID} \
    --zent_image_arn ${ECR_REPOSITORY}:${TAG} \
    --zent_port ${PORT} \
    --zent_service_name ${SERVICE_NAME} \
    --zent_env ${ENV} \
    --zent_dd_team ${TEAM} \
    --zent_cpu ${CPU} \
    --zent_mem ${MEM} \
    --zent_tag ${TAG}

cat task_def.json | jq
mv task_def.json ${DOCKERFILE_PAHT}/

aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin ${ACCOUNT_ID}.dkr.ecr.ap-northeast-2.amazonaws.com

cd ${DOCKERFILE_PAHT}
docker build -t ${ECS_SERVICE_NAME} -f Dockerfile --cache-from ${ACCOUNT_ID}.${ECR_REPOSITORY}:latest ../../
docker tag ${ECS_SERVICE_NAME} ${ACCOUNT_ID}.${ECR_REPOSITORY}:latest
docker tag ${ECS_SERVICE_NAME} ${ACCOUNT_ID}.${ECR_REPOSITORY}:${TAG}

docker push ${ACCOUNT_ID}.${ECR_REPOSITORY}:latest
docker push ${ACCOUNT_ID}.${ECR_REPOSITORY}:${TAG}

container_id=$(docker create ${ACCOUNT_ID}.${ECR_REPOSITORY}:${TAG})
docker cp $container_id:/.next .
docker rm $container_id

aws s3 sync .next/static s3://${S3_BUCKET}/[path]/${ASSET_PREFIX}/_next/static --cache-control max-age=31536000,public,immutable

TASK_DEFINITION_ARN=$(aws ecs register-task-definition \
    --cli-input-json file://task_def.json \
    --query 'taskDefinition.taskDefinitionArn' \
    --output text)

echo "Registered Task Definition: $TASK_DEFINITION_ARN"
          
aws ecs update-service \
    --cluster $ECS_CLUSTER_NAME \
    --service $ECS_SERVICE_NAME \
    --task-definition $TASK_DEFINITION_ARN \
    --force-new-deployment

echo "ECS service $ECS_SERVICE_NAME updated successfully with task definition $TASK_DEFINITION_ARN"

 

여기서 중요한 건,

아래 부분이다.

container_id=$(docker create ${ACCOUNT_ID}.${ECR_REPOSITORY}:${TAG})
docker cp $container_id:/.next .
docker rm $container_id

 

이게 무슨 얘기냐면, Dockerizng을 진행하게 되면 next.config.js 에 기재된 형태처럼

standalone 파일과 .next 폴더가 나오게 된다.

 

이때, standalone 폴더는 ECS 내 올라가야 하고

.next 폴더는 ECS가 아닌 S3 버켓에 올라가서 CDN으로 동작이 되어야 한다.

 

그렇기때문에 Docker Container 내에서 이 폴더를 로컬로 Copy 하여 S3 에 옮기는 작업을 해줘야 한다.

 

 

반응형
반응형

개요

  • Vercel 이 뭐냐?
  • ECS로 왜 넘어가냐?
  • 일정 / 구성을 어떻게 잡아야 하나...

 

Vercel 이 뭐냐?

 

현재 우리회사의 Front는 NEXT.js 를 사용하고 있었고, 그에따른 PaaS를 Vercel을 사용하고 있었다.

나도 Next 라던가, Vercel 이라던가 이런거 잘 모르지만, 참 좋은 Platform? 인 것 같다.

 

자체적으로 환경변수도 지원해주고, CSR, SSR 방식 모두 지원하고

Serverless 형태로 동작하고 비용이나 모니터링 정말 모든걸 All-In-One 으로 해준다. (도메인도...)

 

근데 한가지 문제가 생겼다.

문제발생 1. IP 통제

ISMS 를 진행하면서, IP 통제를 진행해야 했다.

여기서의 IP 통제는 여러가지 방식으로 진행했어야 하는데, 아래 사진과 같다.

 

일단 사내망에서만 접근 가능하도록 IP 통제를 진행했어야 했다 (개발환경 / 스테이징 환경)

물론 Vercel에서도 이는 가능하다 (WAF 기능이 존재함 -> 하지만 이건 문제가 있음)

 

그리고 Vercel 의 SSR은 Serverless 형태로 띄워지기 때문에 IP를 고정시킬 수 없다.

그래서 전통적인 Front - Server 기반의 Architecture 내에서 Server 로 통신하는 IP를 특정시키기에는 어려움이 존재한다.

 

문제발생 2. 비용

 

현재 우리회사도 Vercel Pro를 사용하고 있고, 

몇몇 보안기능도 활용하고 있다.

 

하지만 WAF 기능을 사용하기 위해선 Enterprise 사용해야 했다.

관련해서 Vercel 담당 엔지니어와 논의를 진행해봤지만...... 생각보다 너무 많은 비용이 지불해야 되서 이건 포기하기로 했다.

 

현재 우리 회사에 FE 개발자는 10명정도 존재하고, 프로젝트도 10~20개 정도 존재한다.

그 과정에서 Enterprize 옵션으로 옮기는것이 비용효율적으로 좋진 않았기 때문에 AWS로 이전하기로 결정하였다.

 

ECS로 왜 넘어가냐?

처음부터 ECS로 결정한건 아니었다.

다양한 옵션이 있었고... 많은 논의를 거쳐 Elastic Container Service로 결정을 하게 되었다.

 

Amplify 도 고민을 했었고...
EKS 도 고민했었다...

 

하지만 .. 결국 ECS로 결정했다.

간단하게 결정한 이유를 보면

 

Amplify를 사용하지 않은 이유

Amplify는 Vercel이랑 완전 똑같았다

똑같았기에 그냥 FE분들이 알아서 인프라를 구성하고 기존과 동일하게 할수 있다는 점이 큰 장점이었다.

  • 브랜치 별로 배포 구성 / Domain 자동 연결
  • 환경변수 자체등록
  • Vercel과 같이 PaaS 처럼 Console UI 제공

 

하지만 2가지 문제가 있다고 생각했다.

 

첫번째는 IP를 특정지을 수 없다.

Amplify도 결국 Vercel 처럼 Serverless 형태처럼 Service가 구성되기때문에 고정적인 IP를 알수가 없다.

그래서 Pass

 

두번째는 성숙되지 않았다.

이 부분은 FE 분들과 많이 대립된 부분이긴한데, CloudFormation 스택에서 한번 꼬이면

배포가 20~30분 정도 걸리는 이슈가 많고 아직은 Amplify 를 사용하기에는 좋지 않다고 생각했다.

 

 

EKS를 사용하지 않은 이유

... 간단하다

진짜 우리회사는 EKS를 정말 좋아하지 않는다

뭐... 찬성 1에 반대 20 이니까 뭐 할말이 없음 그래서 ECS로 결정하게 되었다.

 

최종 아키텍처

아키텍처 구성을 간단하게 살펴보면...

총 3가지 구성으로 나뉜다.

 

Front Layer

Front Layer는 말 그대로 NEXT 서버를 SSR 방식으로 서비스를 제공할 수 있도록 하는 구성이다.

  • Application Load Balacncer + Elastic Container Service로 제공한다

 

Static File Serving Layer

SSR 방식의 경우, 정적파일도 서버쪽에서 내려주면 전반적으로 성능이 좋지 않다.

그렇기 때문에 CS / JSS / 이미지 파일같은 경우 CDN을 활용해서 제공하게된다.

  • CloudFront + S3 

 

Middleware / Resource Layer

Next내에서는 점검중 이런 표기를 Middleware 에서 나타낼 수 있다고 한다

이때 Vercel Edge-Config를 사용해서 Json 값을 가져왔다고 하기에, 이부분도 CF + S3로 대체하였다.

 

또한 SSR 방식이다보니, AWS 내부에 위치하는 서버가 되기때문에 Redis ( vercel k/v 대체 ) 를 접근하고

S3 ( vercel blob ) 을 대체하는 방식을 채택하기로 했다.

 

일정 / 구성을 어떻게 잡아야 하냐?

.... 2달 정도 걸렸던 것 같다.

물론 아직까지도 마이그레이션 중이긴한데,,,, 어렵긴 한다

 

반응형
반응형

개요

  • 푸념
  • 사용하지 않는 DB , Table 삭제는 어떻게 진행 되어야 할까?
  • 후기

푸념

ISMS 심사가 몇일 남지 않았다.

한달하고도 몇일 밖에 남지 않았고, 모든일은 부랴부랴 처리되어 가고있다.

 

진짜 매일매일 야근하고 가끔 주말에도 출근을 하다보니 처음부터 잘 구성해놓을걸 이라는 후회와

내 앞길을 닦아놓으셨던 퇴사자들이 가끔은 미워지기도 한다.

 

하지만... 결국은 해내야 하는 업무고

ISMS 는 통과 아니면 실패기 때문에 해야지...


사용하지 않는 DB , Table 삭제는 어떻게 진행 되어야 할까?

 


DB 유저 권한 제어 - 문제인식

일단 기본적인 DB의 권한제어는 DBSafer를 통해서 진행할 것이고, DBSafer는 이미 모든것이 완벽하게 구축이 되어있다.

하지만 문제는....

 

유저별로 과하게 정책이 매겨진것이 문제였다.

show grants for '유저';

1> grant all privileges ...
2> grant select, update, delete ... prod%.* 
3> grant select, update, delete ... prd%.*
4> grant select, update ... create role, delete role '*'.'*'

 

저 4가지의 정책이 매겨진 애들이 문제였다.


DB 유저 권한 제어 - 진행해야 하는 부분

결국 우리가 진행해야 할 부분은 아래와 같았다.

  • 사용하지 않는 DB , Table 을 솎아내기
  • 사용하지 않는 유저 솎아내기
  • 사용하지 않는 DB, Table 권한 제거 - 추후 삭제
  • 사용하지 않는 유저 제거 - 추후 삭제 

DB 유저 권한 제어 - 사용하지 않은 DB, Table 을 솎아내기

사용하지 않은 DB, Table은 Data 팀원 분들이 도와주셨다

Data 팀분들은 아래 쿼리를 통해 Database 내 정책이나 테이블 내 권한을 확인 하였다.

SELECT * FROM information_schema.SCHEMA_PRIVILEGES ;
SELECT * FROM information_schema.USER_PRIVILEGES ;
SELECT * from information_schema.TABLE_PRIVILEGES

 

위 쿼리들과 테이블 내 최종 업데이트 시간을 모아서 

관련한 전체 시트를 만들어서 공유 주셨다


DB 유저 권한 제어 - 사용하지 않은 유저 솎아내기

해당 부분은 Devops 파트내에서 진행하였다.

다행히 이전에 모든 소스코드내에서 매직넘버로 붙어있던 것들을 SSM Paramter Store를 사용하는것으로 바꿔놓았기 때문에,

 

SSM Paramter Store를 Retrive 하는 CLI를 만들어서,

현재 서비스 내에서 참조 하고 있는 유저인지, 아닌지를 확인할 수 있었다.

 

물론, 그럼에도 100% 검출은 안된다.

그래서 몇몇 계정의 대해서는 추후 inactive 모드를 활성화 해서 지켜볼 계획이었다.


DB 유저 권한 제어 - DB, Table 권한 제거 - 추후 삭제

일단 전략은 이러했다.

사용하지 않는 DB, Table 권한은 삭제하고,

사용하지 않는다면 제거 하는 방향으로 하자고 결정이 되었다.

관련해서는 Data Team, Devops Team, CS, Operation Team 모두 모니터링을 해보자고 논의했고

 

많고 많은 테스트와 확인을 거쳤지만,

혹시나 운영상에 조금이라도 문제가 생기면 다시 권한 Rollback 을 해야하자는 마음으로 진행하였다.

 

일단, 위 기재된 4개의 과한 정책을 가진 유저들은 스키마별로 정책을 다시 매겨줬다.

USE sys;

-- 여러 데이터베이스 이름을 쉼표로 나열
SET @databases = 'prod_a,prod_b,prod_c,prod_d';

SET @username = 'dobby';
SET @userhost = '%';

-- 권한 분리
SET @db_privileges = 'SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, REFERENCES, INDEX, ALTER, CREATE TEMPORARY TABLES, LOCK TABLES, EXECUTE, CREATE VIEW, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE';
SET @global_privileges = 'RELOAD, PROCESS, SHOW DATABASES, REPLICATION SLAVE, REPLICATION CLIENT';

-- GLOBAL 권한 먼저 부여
SET @global_grant_stmt = CONCAT('GRANT ', @global_privileges, ' ON *.* TO "', @username, '"@"', @userhost, '";');
PREPARE stmt FROM @global_grant_stmt;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

-- DB별 권한 부여 프로시저
DROP PROCEDURE IF EXISTS GrantDbPermissions;

DELIMITER //

CREATE PROCEDURE GrantDbPermissions()
BEGIN
    DECLARE done INT DEFAULT FALSE;
    DECLARE db_name VARCHAR(255);

    DECLARE cur CURSOR FOR 
        SELECT SUBSTRING_INDEX(SUBSTRING_INDEX(@databases, ',', n), ',', -1) AS db
        FROM (
            WITH RECURSIVE numbers(n) AS (
                SELECT 1
                UNION ALL
                SELECT n + 1 FROM numbers WHERE n <= CHAR_LENGTH(@databases) - CHAR_LENGTH(REPLACE(@databases, ',', '')) + 1
            )
            SELECT n FROM numbers
        ) AS t;

    DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;

    OPEN cur;
    read_loop: LOOP
        FETCH cur INTO db_name;
        IF done THEN
            LEAVE read_loop;
        END IF;
        SET @grant_stmt = CONCAT('GRANT ', @db_privileges, ' ON `', db_name, '`.* TO "', @username, '"@"', @userhost, '";');
        PREPARE stmt FROM @grant_stmt;
        EXECUTE stmt;
        DEALLOCATE PREPARE stmt;
    END LOOP;
    CLOSE cur;

    FLUSH PRIVILEGES;
END //

DELIMITER ;

-- 실행
CALL GrantDbPermissions();

-- 정리
DROP PROCEDURE GrantDbPermissions;

 

위 프로시저를 사용해서, 모든 스키마의 권한을 부여했다.

그리고 기존 매겨진 권한은 revoke 명령어를 사용해서 권한을 제거하는 방향으로 진행하였다.

 

해당 작업이 다 끝나고,

모든 계정의 대해서 하나하나씩 필요없는 DB, Table 별 권한을 삭제했다 (다행히 이슈없었음) - 애초에줄때도 필요한 것만 줘도 됨

 

그리고 모든 DB.Table 별 권한제거를 했다면

아래 쿼리를 통해 다시한번 Double Check를 진행했다.

SELECT 
    u.User AS '사용자',
    u.Host AS '호스트',
    d.Db AS '데이터베이스',
    GROUP_CONCAT(
        DISTINCT 
        CASE 
            WHEN d.Select_priv = 'Y' THEN 'SELECT' 
            WHEN d.Insert_priv = 'Y' THEN 'INSERT' 
            WHEN d.Update_priv = 'Y' THEN 'UPDATE' 
            WHEN d.Delete_priv = 'Y' THEN 'DELETE' 
            WHEN d.Create_priv = 'Y' THEN 'CREATE' 
            WHEN d.Drop_priv = 'Y' THEN 'DROP' 
            WHEN d.Grant_priv = 'Y' THEN 'GRANT' 
            WHEN d.References_priv = 'Y' THEN 'REFERENCES' 
            WHEN d.Index_priv = 'Y' THEN 'INDEX' 
            WHEN d.Alter_priv = 'Y' THEN 'ALTER' 
            WHEN d.Create_tmp_table_priv = 'Y' THEN 'CREATE TEMPORARY TABLES' 
            WHEN d.Lock_tables_priv = 'Y' THEN 'LOCK TABLES' 
            WHEN d.Create_view_priv = 'Y' THEN 'CREATE VIEW' 
            WHEN d.Show_view_priv = 'Y' THEN 'SHOW VIEW' 
            WHEN d.Create_routine_priv = 'Y' THEN 'CREATE ROUTINE' 
            WHEN d.Alter_routine_priv = 'Y' THEN 'ALTER ROUTINE' 
            WHEN d.Execute_priv = 'Y' THEN 'EXECUTE' 
            WHEN d.Event_priv = 'Y' THEN 'EVENT' 
            WHEN d.Trigger_priv = 'Y' THEN 'TRIGGER' 
        END
        SEPARATOR ', '
    ) AS '권한'
FROM mysql.user u
LEFT JOIN mysql.db d ON u.User = d.User AND u.Host = d.Host
WHERE d.Db IS NOT NULL 
and d.Db in ("지워져야 할 DB")
GROUP BY u.User, u.Host, d.Db
ORDER BY u.User, u.Host, d.Db;

DB 유저 권한 제어 - 사용하지 않는 유저 제거 - 추후 삭제 

이건 솔직히 좀 애매한 구석이 있었다.

유저를 삭제하기라도 하는 날에, Rollback이 안된다

왜냐하면... 몇몇 계정들은 비밀번호를 모른다... ( 실제 사용하는지 안하는지 알수가 없음 )

 

그래서 Inactive 기능을 사용하기로 했다.

Inactive 기능을 사용하고 몇일동안 문제가 없다면 삭제 하고, 문제가 있으면 다시 active 하는 방향으로 진행하였다.

## inactive
alter user 'dobby'@'%' account lock;

## active
alter user 'dobby'@'%' account unlock;

## status locked
select user,host, account_locked
from mysql.user
where user = "dobby";

 


후기

보통 검증하는 기간이 2~3주 정도 소요가 되었다.

사용하지 않는 유저를 찾고...

사용하지 않는 DB 찾고...

사용하지 않는 테이블을 뒤지고...

 

그리고 실제 실행과 모니터링하는 시간까지 한 한달가까이 했던 것 같다.

해당 형태가 완성되기까지 같이 협업했던 데이터팀과, 운영팀, CS팀에게 감사를 보낸다.

반응형
반응형

개요

  • GPU 컴퓨팅 고도화
  • Model은 어떻게 관리해야 할까
  • 앞으로 해결해나가야 할 문제

이전 포스팅 

GPU 인스턴스 구성을 구성해보자 - 1

 


GPU 컴퓨팅 고도화

이전 p5 타입의 인스턴스에 gpu를 활성화까지 진행하고, 대체로 잘 동작하고 있었다.

하지만 이게 동작이 결국에 docker-compose 형태로 동작이 되고, 문제 시 Rollback 이나 CI/CD 구조내에서 많은 문제가 있었다.

그에따라 Devops 인원들이 붙어서 배포를 관리해주는 건 너무 비효율적이었다.

 

아무래도 바쁜데 이런것까지 잡아먹힐 수는 없으니, 어떻게 구성하는것이 좋을까 고민하던차에 ECS on EC2 형태가 어떨까... 싶었다.

 


GPU 타입의 ECS Cluster 생성

ECS Cluster 내에서 GPU 옵션이 붙어있는 EC2를 생성할 수 있었다.

원래는 EKS로 구축하려 했으나... 아직은 회사가 EKS를 받아들이기에는 시기상조

 

그리고 띄워봤는데, 띄우자 마자 gpu 활성화 없이 바로 gpu가 활성화 되어있네?

이전에 하루 다 날린 결과물이 허탈할 지경이었다.

이미 활성화 되어있는 gpu
ecs agent 까지 준비완료

좋아 목표는 아래와 같다

  • 기존 CD 구성형태로 ECS 배포 관리
  • ECS Agent를 활용하여 Self healing 구성
  • 배포 알림 시스템 구성

GPU ECS Task Definition

{
    "family": "<family name>",
    "containerDefinitions": [
        {
            "name": "<ecs name>",
            "image": "<image arn>",
            "cpu": 0,
            "memory": <memory>,
            "portMappings": [
                {
                    "name": "<server>",
                    "containerPort": <container port>,
                    "hostPort": 0,
                    "protocol": "tcp",
                    "appProtocol": "http"
                }
            ],
            "essential": true,
            "environment": [
                {
                    "name": "CUDA_VISIBLE_DEVICES",
                    "value": "0"
                },
                {
                    "name": "MKL_NUM_THREADS",
                    "value": "4"
                },
                {
                    "name": "TZ",
                    "value": "Asia/Seoul"
                },
                {
                    "name": "NUMEXPR_NUM_THREADS",
                    "value": "4"
                },
                {
                    "name": "GPU_SETTING",
                    "value": "0"
                },
                {
                    "name": "NUMEXPR_MAX_THREADS",
                    "value": "4"
                },
                {
                    "name": "NVIDIA_DRIVER_CAPABILITIES",
                    "value": "compute,utility"
                },
                {
                    "name": "OMP_NUM_THREADS",
                    "value": "4"
                }
            ],
            "environmentFiles": [],
            "mountPoints": [],
            "volumesFrom": [],
            "ulimits": [
                {
                    "name": "nofile",
                    "softLimit": 65536,
                    "hardLimit": 65536
                }
            ],
            "logConfiguration": {
                "logDriver": "awsfirelens",
                "options": {
                    "apikey": "<datadog api key>",
                    "compress": "gzip",
                    "provider": "ecs",
                    "dd_service": "<server>",
                    "Host": "http-intake.logs.datadoghq.com",
                    "TLS": "on",
                    "dd_source": "inference-server",
                    "dd_tags": "env:<environment>,team:<team>,role:server,app:<server>",
                    "Name": "datadog"
                },
                "secretOptions": []
            },
            "systemControls": [],
            "resourceRequirements": [
                {
                    "value": "1",
                    "type": "GPU"
                }
            ]
        },
        {
            "name": "datadog-agent",
            "image": "public.ecr.aws/datadog/agent:latest",
            "cpu": 0,
            "links": [],
            "portMappings": [
                {
                    "containerPort": 8126,
                    "hostPort": 8126,
                    "protocol": "tcp"
                }
            ],
            "essential": true,
            "entryPoint": [],
            "command": [],
            "environment": [
                {
                    "name": "ENVIRONMENT_NAME",
                    "value": "<environment>"
                },
                {
                    "name": "SERVICE_NAME",
                    "value": "<server>"
                },
                {
                    "name": "DD_API_KEY",
                    "value": "<datadog api key>"
                },
                {
                    "name": "ECS_FARGATE",
                    "value": "true"
                },
                {
                    "name": "DD_APM_ENABLED",
                    "value": "true"
                },
                {
                    "name": "DD_LOGS_ENABLED",
                    "value": "true"
                },
                {
                    "name": "APPLICATION_NAME",
                    "value": "<server>"
                }
            ],
            "mountPoints": [],
            "volumesFrom": [],
            "logConfiguration": {
                "logDriver": "awslogs",
                "options": {
                    "awslogs-group": "<group>",
                    "awslogs-create-group": "true",
                    "awslogs-region": "ap-northeast-2",
                    "awslogs-stream-prefix": "ecs-service"
                }
            },
            "systemControls": []
        },
        {
            "name": "firelens_log_router",
            "image": "amazon/aws-for-fluent-bit:stable",
            "cpu": 0,
            "links": [],
            "portMappings": [],
            "essential": true,
            "entryPoint": [],
            "command": [],
            "environment": [
                {
                    "name": "SERVICE_NAME",
                    "value": "<server>"
                },
                {
                    "name": "ENVIRONMENT_NAME",
                    "value": "<environment>"
                },
                {
                    "name": "APPLICATION_NAME",
                    "value": "<server>"
                }
            ],
            "mountPoints": [],
            "volumesFrom": [],
            "user": "0",
            "logConfiguration": {
                "logDriver": "awslogs",
                "options": {
                    "awslogs-group": "<group>",
                    "awslogs-create-group": "true",
                    "awslogs-region": "ap-northeast-2",
                    "awslogs-stream-prefix": "ecs-service"
                },
                "secretOptions": []
            },
            "systemControls": [],
            "firelensConfiguration": {
                "type": "fluentbit",
                "options": {
                    "config-file-type": "file",
                    "config-file-value": "/fluent-bit/configs/parse-json.conf",
                    "enable-ecs-log-metadata": "true"
                }
            },
            "credentialSpecs": []
        }
    ],
    "executionRoleArn": "arn:aws:iam::<accountId>:role/ecsTaskExecutionRole",
    "networkMode": "bridge",
    "volumes": [],
    "placementConstraints": [],
    "requiresCompatibilities": [
        "EC2"
    ],
    "cpu": "8192",
    "memory": "24576",
    "runtimePlatform": {
        "cpuArchitecture": "X86_64",
        "operatingSystemFamily": "LINUX"
    },
    "enableFaultInjection": false
}
  • 기존 ECS Task Definition 과 동일하나 GPU 옵션이 몇몇 추가되었다.
  • 또한 DataDog 을 사용하는 만큼, 관련 SideCar도 추가하였다.

결과는? -> 잘 동작함 

이참에 이렇게 할걸 ... 약간 후회


Model은 어떻게 관리해야 할까?

EC2 on ECS 로 구성을 하고, 나서 큰 문제는 없었으나 아래와 같은 이슈가 발생하였다.


모델 추가 변경의 건 - 첫번째 시도 (Git LFS)

Dev : 모델업데이트를 해야되는데 어떻하죠?

Devops : 모델은 VCS에 못올리나요?

Dev : 모델자체가 크기가 커서 에러가 납니다.

Devops : Git LFS로 올려야될것 같네요. 확인해보겠습니다.

 

모델을 생각못한건 아니지만, 아니 솔직히 생각못하긴 했다.

그래서 모델을 Git LFS를 사용하기로 했다.

git lfs install

git lfs track "~~.pt"
git lfs track "~~.lib"
git lfs track "~~.dll"

>> 산출물

## .gitattributes

* text = auto
~~.pt filter=lts diff=lfs merge=lfs -text
~~.lib filter=lts diff=lfs merge=lfs -text
~~.dll filter=lts diff=lfs merge=lfs -text

근데, 이걸 설정하고 CD를 진행하려 보니 문득 이런생각이 들었다.

 

LFS 사용한다면?

  • Git LFS를 설정 -> Git Storage 비용 발생 -> CD 내에서 LFS 관련 옵션 설정 -> 어차피 CD 할때마다 Pull / Push 함...

S3 사용한다면? 

  • 필요 모델 S3에 올림 -> S3 비용발생 -> CD 내에서 S3 파일다운받아서 해제 
Devops : ~~님, 혹시 모델 변경이 잦을까요?

Dev : 아뇨, 잦지는 않을 것 같습니다. 현재는 개발중이라 수정이 될텐데
추후에는 한달에 한번정도만 수정 될 것 같아요

Devops : OK

모델 추가 변경의 건 - 두번째 시도 ( S3 사용하자)

 

S3로 선택한 건, 더 편하고 비용친화적이고 관리하기 더 편할것같아서 

머리아픈 LFS 보다는 S3를 선택하기로 했다.

 

CD 단에서 S3 데이터를 풀어서 Dockerizing 하면 끝이라고 생각했다.

## action 내에서 발췌

- name: Pull Models
  run: |
   aws s3 cp s3://<S3 Bucket>/models.zip .
      
- name: Unzip Models
  run: |
   unzip -o models.zip

 

오... 잘 동작하는데?


앞으로 해결해나가야 할 문제

  • GPU 인스턴스 내 배포문제
    • 현재 p5.xlarge, p5.2xlarge 타입을 사용하고 있는데
    • 각각 사용할 수 있는 GPU는 1개씩이다.
    • 이 얘기는 배포 시, Rolling Update가 안된다.

이 문제의 대해서는 일단 최소 인스턴스를 2개 운영하는 방식으로 구성하기로 했다.

물론 개발환경만 DeploymentConfiguration 을 조절해서 Recreate 방식으로 수정했다.

운영은 다운타임이 발생해서 일단 좋은방법이 나타날때까지 GPU 인스턴스를 2개 운영하는것으로 대체...

 

  • 운영환경 내 비용효율화 문제
    • 일단 개발환경은 SPOT 인스턴스로 대체하여, 어느정도의 비용은 낮췄다.
    • 그외 더 좋은 방법이 없을까 고민했지만 -> 애초에 GPU 옵션을 사용하는 서비스에서 비용효율화... 가능할까?
  • 더 좋은 방법 없을까?
    • SageMaker를 고려중이다.
    • 이건 좀더 공부해봐야 할 것 같다.
반응형

+ Recent posts