문서 검색

개발자를 위한 쉬운 쿠버네티스

실습 환경

🛠️

사용하는 기술

  • 클러스터: GKE
  • 리전/존: us-west-1
  • 오토파일럿 클러스터 사용

쿠버네티스 클러스터


💡

쿠버네티스 클러스터와 쿠블릿

쿠버네티스 클러스터는 컨테이너가 실행되는 컴퓨트 인스턴스인 노드의 집합이며 각 노드에는 kubelet 이라고 하는 특수한 쿠버네티스 프로세스가 실행되고 있다.

kubelet 은 컨트롤 플레인이라고 하는 쿠버네티스의 두뇌와 같은 프로세스와 통신하고 컨테이너 런타임을 통해 노드에서 실행되는 컨테이너의 생명주기 관리를 담당한다.

컨트롤 플레인은 노드에 특정 컨테이너를 실행하도록 지시하지만, 컨테이너의 실제 실행은 노드의 역할이다.

그래서, 노드는 충돌이 발생한 컨테이너를 재시작하거나 노드의 리소스가 부족할 때 메모리를 회수하는 등의 일부 작업을 컨트롤 플레인에게 허락을 받지 않고 자체적으로 수행한다.

파드

파드는 주로 단일 컨테이너로 구성 되어 있지만, 같은 성격의 컨테이너를 함께 배포하는 경우에는 파드 한 개에 여러 개의 컨테이너로 구성될 수 있다.

디플로이먼트

쿠버네티스에서 파드를 독립적으로 생성하고 관리할 수 있지만, 일반적으로 장애에 대한 자가치유가 불가능하기 때문에 추상화 된 상위 개념인 디플로이먼트를 사용 한다.

쿠버네티스는 자원을 생성할 때 사용자가 요청한 내용을 기억하고, 관찰된 상태를 사용자가 요구하는 상태 즉, 원하는 상태로 유지하기 위해 노력을 기울이는데 이러한 성질 덕분에 관찰된 파드의 개 수가 요구했던 개 수보다 부족할 경우 새로운 파드를 스케줄링한다.

덕분에 어떠한 이유로 종료된 파드를 직접 복구하지 않고 자동으로 관리할 수 있게 되어 디플로이먼트를 사용해 서비스의 생명 주기를 관리한다.

디플로이먼트를 작성하는 YAML 매니페스트 내용에서는 matchLabels 섹션이 존재하고, 파드를 생성하는 YAML 매니페스트 파일에는 labels 섹션이 존재한다.

이는 파드 객체와 디플로이먼트는 서로 다른 자원이기 때문에, 디플로이먼트가 각각의 파드를 참조할 방법이 필요하다.

쿠버네티스는 각 파드가 레이블 정보를 갖도록 스펙을 정의 하여 디플로이먼트와 동일한 레이블을 참조하도록 해결하고 있다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: timeserver
spec:
  replicas: 3
  selector:
    matchLabels:
      pod: timeserver-pod
  template:
    metadata:
      labels:
        pod: timeserver-pod
    spec:
      containers:
      - name: timeserver-container
        image: docker.io/wdenniss/timeserver:1

서비스

서비스는 외부 사용자가 접근해야 하는 단일 액세스 포인트를 제공하고, 연결 되어 있는 여러 파드에 트래픽을 분산 시킨다.

서비스에는 내부 IP 주소와 DNS 레코드가 할당되기 때문에 클러스터 내부의 다른 파드에 참조할 수 있으며, 외부 IP 주소도 할당 된다.

서비스는 디플로이먼트를 참조하지 않으며 실제로도 디플로이먼트의 존재에 대해 알지 못한다. 대신, 서비스는 디플로이먼트나 파드가 레이블 정책을 사용하여 매칭 했던 것 처럼 셀렉터라는 개념을 사용하여 참조하게 된다.

apiVersion: v1
kind: Service
metadata:
  name: timeserver
spec:
  selector:
    pod: timeserver-pod
  ports:
    - port: 80
      targetPort: 80
      protocol: TCP
  type: LoadBalancer
💡

회사에서 쿠버네티스를 운영하며 만난 이슈

사내에서 GitOps 전략을 사용중으로 Service 자원에 대한 매니페스트 설정은 대부분 Helm Chart 템플릿 렌더링 방식으로 이루어진다.

차트 템플릿을 구성할 때 단일 서비스에 대해서는 불필요한 YAML 구조에서 키를 생성 할 필요 없이 바로 값을 입력할 수 있도록 기본 값을 제공했다.

그렇게 구성된 템플릿이 어느 날 하나의 프로젝트 안에서 여러 서비스가 구성 되어야 하는 것을 알게 되었고, 파드와 통신을 이어줄 때 서로 의미가 다른 파드이기 때문에 통신이 서로에게 흘러가지 않도록 막아야했다.

그 때 셀렉터를 통해 파드의 기본 키 값을 사용했는데, 이 값이 훗날 기본 값을 모두 제거하고 명시적으로 변경했을 때 셀렉터가 파드를 바라보지 못하여 트래픽이 흐르지 않았던 장애가 있었다.

쿠버네티스 운영


파드 상태 감지

쿠버네티스는 파드를 생성할 때 그리고 파드가 생성된 이후에 애플리케이션이 실행중일 때 오류가 발생하면 이를 감지하여 자동으로 재시작 한다.

기본적으로 디플로이먼트에 기록 했었던 레플리카 설정이 부족한 것을 인지하고 가용 가능한 공간에 새로운 파드를 생성할 것이다.

프로세스가 멈췄거나 커넥션이 맺어지지 않는 웹서비스거나 접근이 불가능한 외부 서비스의 에러는 어떻게 처리할까?

우선 쿠버네티스를 사용하면 이 질문이 정말 가볍게 느껴질 정도로 적절하게 감지하고 장애 발생 시 복구를 시도할 수 있다.

기본적으로 상태를 검사하는 프로세스를 활성 프로브(liveness probe) 및 준비성 프로브(readiness probe)라고 부른다.

이 프로브는 컨테이너의 상태를 쿼리하고, 컨테이너는 자체 내부 상태를 확인하여 모든 것이 정상일 경우 성공 코드를 반환 한다. 만약, 타임아웃이 발생하거나 컨테이너 자체에 문제가 있다고 판단 하는 경우 프로브는 실패로 간주한다.

liveness probereadiness probe
실패로 간주 될 경우파드가 종료되고 재시작 된다.프로브가 통과할 때 까지 파드는 생성되지 않고 트래픽 대상에서 제외된다.
상태 검사 의미컨테이너가 정상적으로 실행중인가?컨테이너가 트래픽을 받을 준비가 되었는가?

이 책에서 프로브를 지칭할 때 활성 프로브 또는 준비성 프로브라고 부른다.

위 표를 보고나면 두 상태 검사 프로세스가 다른 지표를 검증하는 프로세스라는 것을 알 수 있다.

준비성 프로브는 부팅 시 상태를 확인하기 위해 설정하고, 활성 프로브는 컨테이너가 정상적으로 동작하는 상태를 확인하기 위해 설정 한다.

💡

준비성 프로브 (readiness probe)

준비성 검사가 없다면 쿠버네티스는 컨테이너가 트래픽을 수신할 준비가 되어있는지 전혀 알 수 없다.

만약 시작된 컨테이너에 트래픽을 즉시 전송한다면 파드가 기동되면서 애플리케이션이 프로세스로 떠서 초기 설정 정보를 불러오고 있는데 트래픽을 대뜸 받게 되면 쿠버네티스 입장에서는 트래픽 손실이 발생하는 것이 된다.

이 문제를 해결하기 위해 상태 검사 프로브가 존재하고, 내부 테스트가 통과한 경우에만 Ready 상태로 보고해 이 문제를 사전에 예방한다.

💡

활성 프로브 (liveness probe)

컨테이너가 현재 트래픽을 수신할 준비가 되지 않았다라는 상태를 검증할 수 있다. 그 중 가장 좋은 예시로는 데이터베이스 연결 또는 외부 의존성을 기다리고 있는 컨테이너를 대상으로 설정 할 때 의도에 맞게 사용할 수 있다.

프로브 유형

HTTP

HTTP 서비스를 제공하는 웹 애플리케이션 서비스에는 HTTP 타입을 대상으로 헬스 체크와 같은 엔드포인트를 노출하여 사용할 수 있다.

TCP

TCP 기반 소켓 서비스 또는 SMTP 와 같은 서비스에는 TCP 타입을 대상으로 커넥션이 맺어지는 경우 프로브가 성공으로 간주된다.

Bash Script

만약 HTTP 나 TCP 같이 네트워크 계층을 외부에 노출하지 않는 서비스라면(배치 잡과 같은) 배시 스크립트를 사용하여 상태 검사 프로브를 설정할 수 있다.

애플리케이션 업데이트하기

쿠버네티스의 롤링 업데이트 전략을 사용하여 제로 다운 타임 애플리케이션 배포 전략을 선택할 수 있다. 그 외에도 다양한 배포 전략이 있다.

쿠버네티스가 제공하는 가장 기본적인 제로 다운타임 업데이트 전략으로, 롤링 업데이트 설정 값을 기준으로 신규 버전으로 구성 된 파드 그룹을 생성하고, 이 그룹이 사용 가능한 상태가 될 때 까지 기다린 다음 설정 값 만큼 구 버전 파드를 종료 시키며 최종적으로 모든 파드가 신규 버전으로 전환 되는 전략이다.

구 버전의 모든 파드를 삭제하고 신규 버전의 대체 파드를 스케줄링 하는 방식으로, 제로 다운타임 방식이 아니다.

재생성 전략의 장점은 롤링 업데이트에서 구 버전과 신규 버전이 하위 호환되는 기능을 제공해야 했다면, 여기서는 동시 지원해야 하는 호환 이슈가 없으며 컴퓨팅 용량이 추가로 필요하지 않다.

이 전략은 다운타임이 보장 될 필요가 없는 개발 및 검증 환경에서 유용하며 운영 환경에 적용하는 것은 피해야 한다.

신규 버전이 구 버전과 함께 배포 되는 롤아웃 전략이다. 이 전략은 신규 버전이 완전히 배포와 테스트가 완료되고, 서비스 할 준비가 되고 나면 구 버전의 애플리케이션을 사용 중단하고 신규 버전을 통해 서비스가 제공 된다.

블루 그린 전략의 장점은 문제가 발생 할 경우 배포를 빠르게 취소하여 원복시킬 수 있고, 서비스를 유지한 채 모든 것이 안정된 다음 구 버전을 제거한다.

반면에, 구 버전과 신규 버전이 모두 다 배포 되어야 하기 때문에 노드 자원을 두 배로 소모하고 쿠버네티스에서 이를 직접적으로 지원하지 않기 때문에 ArgoRollouts, Spinaker, Istio 같은 다른 도구와 조합해야 한다.

설명에는 없지만 Canary 배포 전략으로 임의의 가중치 만큼 신규 버전 애플리케이션으로 트래픽을 발생 시켜 검증하고, 점진적으로 배포해 나가는 전략도 존재한다.

블루 그린의 장점을 그대로 사용하면서 단점이었던 컴퓨팅 리소스 부분에서 최적화 되어 현대 가장 많이 쓰이는 배포 전략이기도 하다.

리소스 관리

파드 스케줄링

쿠버네티스 스케줄러는 파드를 노드에 할당할 때, 클러스터에서 적합한 노드를 찾고 파드에서 컨테이너를 실행하는 것이 스케줄러의 역할이다.

Deployment 객체의 경우, 시스템 상태를 사용자가 요청한 대로 만들기 위해 시스템을 지속적으로 모니터링 한다.

파드 재시작은 기본적으로 liveness, readiness probe 의 상태 검사 실패로 인한 사유로 발생되고, 쿠블릿에 의해 노드에서 로컬로 처리 되며 스케줄러는 이 상태를 감지하게 될 경우 파드를 재할당 하게 된다.

만약, 스케줄러가 파드를 배치하려고 지속적으로 모니터링 해봐도 가용 자원이 없는 상황의 파드는 Pending 상태로 변경 된다.

파드 리소스 선언하기

spec:

  ...
  resources:
    requests:
      cpu: 200m
      memory: 250Mi
    limits:
      cpu: 300m
      memory: 400Mi

위 예제에서 CPU 표기는 200 밀리코어, 즉 코어 1개의 20%를 의미한다. 그리고, 다른 표기법으로는 0.2 와 같이 소수점을 사용할 수도 있다.

쿠버네티스 실무자들 사이에서 밀리코어를 사용하는 표기법이 표준으로 사용되고 있고, 메모리는 Mi(메비바이트/MiB), Gi(기가바이트) 를 사용하며 1024의 거듭제곱 형태를 사용한다.

  • 만약, MG 표기법을 사용하면 메가비이트와 기가비이트 1,000 의 거듭제곱 형태를 사용하게 된다.

파드에 대한 리소스를 명시하지 않더라도 정상적으로 기동이 되는데 그렇게 되면 노드의 리소스가 랜덤하게 할당 된다.

  • 쿠버네티스에게 노드의 용량과 파드의 요구사항을 매치하는 데 필요한 정보를 제공하기 때문에 리소스를 명시하는 것이 중요하다.
💡

파드에 리소스를 정의하지 않는다면 어떻게 될까?

파드에 리소스에 대한 선언이 없다면 리소스가 부족해지거나, 노드에서 사용 가능한 것보다 더 많은 메모리르 사용하는 경우 해당 파드는 축출 대상의 1순위가 된다.

스케줄러가 파드를 노드에 배치할 때 파드의 요청 값 만을 고려하여 노드에 배치한다. 이 때, 요청 값 보다 더 많은 리소스를 사용하게 되는 경우 파드를 처리하는 방법이 다르다.

  • 메모리를 초과하는 경우: OOM Killed 가 발생하고 파드를 재시작한다.
  • CPU를 초과하는 경우: 요청값과 동일한 수준의 CPU 사용량으로 제한된다.
💡

워크로드의 서비스 품질 수준

GUARANTEED 클래스

파드 리소스 선언문에서 requestslimits 를 동일하게 설정한다. 이 설정은 파드가 요청한 리소스만큼만 사용하도록 보장하기 때문에 가장 안정적이다.

스케줄링된 것 보다 많은 리소스는 사용할 수 없기 때문에 파드 축출 대상에서 가장 낮은 우선순위가 된다.

BURSTABLE 클래스

파드 리소스 선언문에서 requests 보다 limits 를 더 높게 설정한다. 이 설정은 파드에 일시적인 트래픽이 발생하여 요청 값 보다 더 많은 자원이 필요할 경우 일시적으로 버스트 해서 사용할 수 있다.

의도치 않게 버스팅 된다면 노드의 가용 자원에 따라 파드 축출이 되거나 CPU 사용량 제한 등 예기치 못한 결과가 발생할 수 있다.

  • 요청 값 만큼의 자원만 사용한다면 파드 축출로 부터 안전할 수 있다.

BEST EFFORT

파드 리소스 선언문에서 requestslimit 값을 설정하지 않는다. 이 설정은 매우 낮은 request 를 갖는 Burstable 클래스와 동일하게 동작하며, 쿠버네티스가 원하는 곳에 스케줄링 할 수 있다.

이 설정은 가장 낮은 등급으로 리소스 경합 상황이 발생하면 가장 먼저 축출 대상이 되고 노드의 자원을 제한 없이 쓸 수 있기 때문에 사용하지 않는 것이 좋다.

리소스 경합

메모리와 같이 노드의 자원에서 유한하게 사용하는 자원을 요청할 때 경합이 발생하는데, 이 경우 쿠버네티스는 축출이라는 프로세스를 통해 요청된 값 이상으로 리소스를 사용하는 파드를 제거하여 노드의 안전성을 보장한다.

축출이 된 파드는 Evicted 상태로 변하며, 이런 파드 상태가 가끔 발생하면 일시적인 트래픽으로 넘어갈 수 있지만 자주 발생할 경우 컴퓨팅 용량을 늘리는 것을 검토해봐야 한다.

축출할 파드를 찾을 때 쿠버네티스는 먼저 파드가 요청한 값보다 더 많은 리소스를 사용하는 파드를 고려하고, 우선순위 번호에 맞춰 정렬한다.

축출 순서는 먼저 우선순위에 따라 정렬된 다음 사용량이 요청값보다 많은지를 고려하여 정렬하는데 파드 스펙을 선언할 때 PriorityClass 타입을 사용하여 적용한다.

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: high-priority
value: 1000
preemptionPolicy: never
globalDefault: false
description: "Critical services."

preemptionPolicy 우선순위 클래스는 클러스터에 사용 가능한 용량이 없는 경우, 우선순위가 낮은 파드를 제거하지 않는다.

apiVersion: apps/v1
kind: Deployment
...
spec:
  priorityClassName: high-priority
  containers:
    ...

우선순위 번호는 파드 축출 대상을 선정할 때 정렬 요소로만 쓰이지 않고, 스케줄링 대상을 선정할 때도 사용된다.

스케줄러는 우선순위가 가장 높은 파드를 우선적으로 스케줄링하며, 특히 배치 잡을 먼저 실행해야 하는지 결정할 때 유용하다.

이와 독립적으로 동작할 수 있도록 쿠버네티스에서는 선점이라는 개념을 사용하며 위 예제에 있듯이 preemptionPolicy 필드를 통해 관리할 수 있다.

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
...
preemptionPolicy: PreemptLowerPriority

해당 예제에서 사용한 우선순위 속성은 사용 가능한 용량이 없는 경우, 우선순위가 가장 낮은 파드를 먼저 제거하도록 한다.

운영 환경으로

스케일링 업

쿠버네티스에는 기본적으로 확장이 필요한 두 개의 리소스가 존재한다.

  1. 파드
  2. 노드

이 두 개의 리소스는 서로 반비례하게 스케일링을 시도하면 의미가 없기 때문에, 파드와 노드를 동시에 그리고 올바른 비율로 확장하는 전략이 필요하다.

파드를 수동으로 확장 및 축소하기

평소보다 많은 트래픽을 처리하고 싶다면 우선 파드의 레플리카 수를 늘려봐야 한다. 이 때, 너무 많은 레플리카를 추가하게 될 경우 클러스터에 해당 파드가 스케줄링 될 공간이 부족하게 된다.

이럴 경우 여러 개의 파드가 Pending 상태로 전환 되어 스케줄링을 대기하고 있는데, 이 때 노드를 확장해야 한다.

kubectl explain pod {pod-name}

해당 명령어를 통해 Insufficient CPU 또는 FailedScheduling 상태가 확인 된다면 노드의 확장이 필요한 상황이다.

반대로, 노드를 스케일 인 해야하는 경우는 축소 대상이 되는 노드에 새로운 파드가 스케줄링 되지 않도록 노드를 차단(cordon) 한 다음, 노드에 있는 파드를 제거(drain) 하여 축소 과정을 거쳐야 한다.

💡

커든과 드레인이란?

  • 커든: 파드를 더이상 노드에 스케줄링 되지 않도록 제한하는 것을 의미한다.
  • 드레인: 쿠버네티스 파드를 안전하게 제거하는 방법으로 더이상 노드에 할당되지 않도록 설정하며 graceful shutdown 방식으로 서비스를 종료한다. 참고

오토스케일링 활용하기

쿠버네티스에서 파드의 레플리카 수를 확장하는 또 다른 방법은 HPA(Horizontal Pod Autoscaling) 을 활용하면 된다.

CPU 사용량과 같은 파드의 메트릭을 관찰하고, 특정 값을 목표하여 파드의 수평적 확장을 구성할 수 있다.

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: timeserver

spec:
  minReplicas: 1
  maxReplicas: 10

metrics:
- resource:
  name: cpu
  target:
    averageUtilization: 20  # CPU 사용률이 이 값 보다 높은 경우 레플리카 확장
    type: Utilization
  type: Resource

scaleTargetRef:  # 확장 될 디플로이먼트 명시
  apiVersion: apps/v1
  kind: Deployment
  name: timeserver
💡

외부 메트릭 지표 활용하기

스케일 아웃을 하기 위해 널리 사용되는 메트릭 중 하나는 RPS(Requests per Second) 지표이다. 이는 레플리카 1 개가 초당 처리할 수 있는 요청 수를 측정하고, 현재 요청의 수를 이 값으로 나누면 필요한 복제본의 수가 된다.

replica_count = RPS / replica_capacity

RPS 메트릭은 클러스터 모니터링 지표를 직접적으로 제공하지 않고 외부 솔루션을 활용해야 하며 이 지표를 HPA 에 적용할 수 있다.

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
...
metrics:
  - type: External
    external:
      metric:
        name: loadbalancing.googleapis.com|https|request_count
        selector:
          matchLabels:
            resource.labels.forwarding_rule_name: "k8s2-fr-21mgs2fl"
    target:
      type: AverageValue
      averageValue: 5

노드를 자동으로 확장하기

클러스터 오토스케일링은 쿠버네티스가 기본적으로 제공하지 않지만, 대부분의 클라우드 제공업체는 이 기능을 제공하고, 노드의 수를 확장하는 기능이 플랫폼의 내장되어 있다.

수동으로 확장하는 경우, 사전에 정의된 기존 설정의 새 노드만을 추가하여 사용할 예정인 노드의 유형을 직접 정의해야 한다.

클러스터 오토스케일링은 HPA 와 같은 파드 기반 확장 기능과 결합해 사용하면 운영 과정에서 프로덕트에 집중할 수 있을 정도로 자동화 된다.

💡

클러스터 오토스케일링을 통한 예비 용량 확보

노드 자동 확장의 단점 중 하나는 오토스케일러가 상황을 너무 잘 판단하고 조정해 버려서, 서버가 기동되고 일정 트래픽을 고려하지 못하여 여유 용량이 부족해지는 경우가 있다.

이러한 문제를 모두 해결하는 한 가지 방법은 우선순위가 낮은 플레이스홀더 파드를 사용하는 것이다.

  • 이 파드는 용량을 예약하는 것 외에는 아무런 기능도 수행하지 않고, 대기 상태로 실행된다.
apiVersion: apps/v1
kind: Deployment
metadata:
  name: placeholder

spec:
  replicas: 10  # 10 개의 플레이스 홀더 파드 예약
  selector:
    matchLabels:
      pod: placeholder-pod

  spec:
    priorityClassName: placeholder-priority
    terminationGracePeriodSeconds: 0
    containers:
    - name: ubuntu
      image: ubuntu
      command: ["sleep"]  # 아무런 작업도 하지 않도록 대기 상태로 설정
      args: ["infinity"]
    resources:
      requests:  # 플레이스홀더 파드에서 사용 할 리소스
        cpu: 200m
        memory: 250Mi

쿠버네티스에서 확장 가능한 앱 구축하기

확장이 필요한 시점에 부랴부랴 확장 방법을 설계하면 시기가 너무 늦다는 점을 기억하자.

스테이트리스 설계

상태를 저장하지 않는 설계 방식은 실행 중인 애플리케이션이 여러 파드가 서로간의 로컬로 저장된 데이터를 참조하지 않고 들어오는 요청을 처리할 수 있는 설계 방식이다.

12factor.net
유명한 Twelve-Factor 앱 설계 방법론

쿠버네티스는 컨테이너가 디스크에 쓴 모든 데이터는 기본적으로 컨테이너가 종료되거나 재시작되면 삭제된다.

  • 퍼시스턴트 볼륨과 스테이트풀을 사용하는 경우는 예외이다.

그러므로, 쿠버네티스에서 관리하는 디스크에 상태를 저장하는 대신 구조화된 데이터는 SQL 또는 NoSQL 에 저장하고, 파일은 오브젝트 스토리지에, 세션 상태는 레디스와 같은 메모리 DB 처럼 외부 데이터 저장소에 저장하는 것을 추천한다.

쿠버네티스의 내부 서비스

내부 서비스에서 사용되는 두 가지 서비스 유형은 ClusterIPNodePort 이다.

ClusterIP 유형은 쿠버네티스 클러스터에서 사용할 수 있는 사설 IP 주소를 제공하며, 클러스터 내의 모든 파드에서 이 IP 주소를 통해 접근할 수 있다. NodePort 유형은 클러스터의 각 노드에서 사용되는 높은 번호의 포트 번호를 예약해 클러스터의 외부에서 접근할 수 있도록 한다.

네트워크

각 파드는 자체 IP 주소를 가지며 NAT 없이도 클러스터의 다른 파드와 직접 통신할 수 있다.

노드에는 VM의 네트워크 인터페이스에 할당되는 자체 IP 주소가 있는 반면, 파드 IP는 트래픽이 노드의 인터페이스를 통해 라우팅되는 가상 네트워크 인터페이스를 사용한다.

  • 노드: 공인 IP, 파드: 사설 IP

그 외에도 서비스 에는 가상 IP 주소가 할당되고, 단일 파드나 노드로 라우팅 되지 않고 서비스 뒤에 위치한 파드에게 트래픽을 분산시키는 역할을 한다.

이 네트워크 컴포넌트(iptablesIPVS 를 사용)는 쿠버네티스에서 제공되며 트래픽에 대한 라우팅을 처리한다.

💡

서비스 네트워크 라우팅

서비스로 요청이 이루어지면 해당 요청은 먼저 노드에 위치한 네트워크 컴포넌트에 의해 처리 된다.

이 컴포넌트는 해당 서비스(서비스 및 파드가 위치한 노드)에 속하는 모든 파드의 쿠버네티스 컨트롤러에 의해 업데이트된 목록을 가지고 있다.

네트워크 컴포넌트는 파드 IP 중 하나를 무작위로 선택하고 노드를 통해 해당 파드로 요청을 라우팅 한다.

서비스 탐색

서비스의 IP 주소를 얻어와서 애플리케이션에 하드코딩 하는 것도 가능하지만, 이렇게 구현하면 서비스가 재시작되거나 다른 노드로 이동할 때 IP 주소가 변경되어 참조하는 코드를 매번 업데이트 해야 한다.

쿠버네티스는 파드에서 서비스를 탐색하는 방법으로 두 가지를 제공한다. 첫 번째 방법은 DNS 조회, 두 번째 방법은 환경 변수를 사용하는 것이다.

  • DNS 조회는 클러스터 전체에서 동작하는 반면, 환경 변수는 동일한 네임스페이스에 위치한 파드에서만 동작한다.
PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=timeserver-5dc65888c-2rmwb
LANG=C.UTF-8
GPG_KEY=7169605F62C751356D054A26A821E680E5FA6305
PYTHON_VERSION=3.12.9
PYTHON_SHA256=7220835d9f90b37c006e9842a8dff4580aaca4318674f947302b8d28f3f81112
PYTHONUNBUFFERED=1
KUBERNETES_PORT_443_TCP=tcp://34.118.224.1:443
KUBERNETES_PORT_443_TCP_PROTO=tcp
KUBERNETES_PORT_443_TCP_PORT=443
KUBERNETES_PORT_443_TCP_ADDR=34.118.224.1
KUBERNETES_SERVICE_HOST=34.118.224.1
KUBERNETES_SERVICE_PORT=443
KUBERNETES_SERVICE_PORT_HTTPS=443
KUBERNETES_PORT=tcp://34.118.224.1:443
HOME=/root
apiVersion: apps/v1
kind: Deployment
metadata:
    name: timeserver
spec:
    replicas: 1
    selector:
        matchLabels:
            pod: timeserver-pod
    template:
        metadata:
            labels:
                pod: timeserver-pod
        spec:
            containers:
            - name: timeserver-container
                image: docker.io/wdenniss/timeserver:5
                env:
                - name: AVATAR_ENDPOINT
                  value: http://$KUBERNETES_SERVICE_HOST

환경 변수 방식을 사용하, kubectl exec 명령어를 통해 파드에 접속하여 env 명령어를 실행하면 서비스의 호스트 주소를 알 수 있고, 이 주소를 통해 빠르게 접근이 가능하다.

그리고, 파드가 서비스의 환경 변수를 받기 위해서는 반드시 서비스가 파드보다 먼저 생성 되어야 한다. 이렇게 사용하면, 변수를 참조하는 일반적인 방법 중 하나는 디플로이먼트에 정의된 자체 환경 변수를 통해 내부 서비스의 전체 HTTP 엔드포인트를 제공하는 것이다.

환경 변수를 이용했을 때 장점은 만약 도커 환경에서 독립적으로 실행해야 하거나 로컬 환경에서 테스트를 진행해야 하는 경우, 환경 변수를 통해 서비스의 엔드포인트를 쉽게 변경할 수 있다는 점이다.

DNS 를 이용한 서비스 탐색은 클러스터의 내부 DNS 서비스를 이용하는 것이다. 이 방법을 이용하게 될 경우, 파드와 다른 네임스페이스에서 실행되는 서비스를 탐색하고자 하는 경우 유일하게 사용할 수 있는 옵션이다.

반면에 해당 접근 방식의 단점으로 DNS 조회를 위해 추가적인 네트워크 요청이 발생하고 이 과정이 약간 느리다는 것이다.

하지만 대부분의 쿠버네티스 클러스터의 경우 이 DNS 서비스가 동일한 노드에서 실행되기 때문에 속도가 매우 빠르다.

인그레스: 로드 밸런싱

기존 서비스 유형에서 생성했었던 로드밸런서는 OSI 4계층 L4 로드밸런서를 제공한다.

로드밸런서를 통해 HTTP 서비스를 노출하는 경우 자체적으로 TLS 터미네이션을 구현해야 하고, 서비스에 대한 라우팅을 matchLabels 규칙에 맞춰 직접 구현해야 한다.

  • 두 개 이상의 개별 서비스를 동일한 로드 밸런서에 직접 노출하는 옵션은 존재하지 않는다. 그렇기 때문에 로드밸런서 유형의 서비스를 여러개 생성하면 각각의 서비스에 대해 로드밸런서가 생성되어 비용이 증가한다.

이 불편함을 소위 7계층의 로드밸런서를 사용하면 해결할 수 있다. 쿠버네티스에서는 인그레스라는 개념을 사용하여 L7 로드밸런서를 제공한다.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
    name: timeserver-ingress
spec:
    rules:
    - http:
        paths:
        - path: /
            pathType: Prefix
            backend:
                service:
                name: timeserver-internal
                port:
                    number: 80
        - path: /robothash
            pathType: Prefix
            backend:
                service:
                name: robothash-internal
                port:
                    number: 80
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
    name: timeserver-ingress
spec:
    rules:
    - host: timeserver.example.com
        http:
            paths:
            - path: /
                pathType: Prefix
                backend:
                    service:
                    name: timeserver-internal
                    port:
                        number: 80
    - host: robothash.example.com
        http:
            paths:
            - path: /
                pathType: Prefix
                backend:
                    service:
                    name: robothash-internal
                    port:
                        number: 80

이 인그레스를 참조하는 서비스는 NodePort 유형으로 사용해야 한다.