kimyu0218
  • [k8s] 쿠버네티스 시작하기 (클러스터링/오케스트레이션/도커 스웜/파드/레플리카셋/디플로이먼트/서비스)
    2024년 06월 20일 16시 21분 38초에 업로드 된 글입니다.
    작성자: @kimyu0218

    클러스터링 & 오케스트레이션

    클러스터링은 여러 대의 컴퓨터를 하나로 묶어 마치 하나의 컴퓨터처럼 사용하는 것을 의미한다. 이때 단일 컴퓨터를 노드라고 부른다. (여러 개의 노드가 모여서 하나의 클러스터로~) 오케스트레이션은 여러 대의 클러스터에서 컨테이너를 자동으로 배포/관리하는 것을 말한다. 오케스트레이션 도구로는 쿠버네티스, 도커 스웜, 아파치 메소스 등이 있다.

    🎻🎹🪈 : 특정 역할 담당 = 여러 연주자 (노드) 로 구성된 클러스터
    🎼 : 연주자 조율 = 컨테이너를 배포하고 관리하는 오케스트레이션
    🤯 기존 방식의 문제점
    • 확장성 부족 : 하드웨어 성능의 한계로 인해 쉽게 확장할 수 없으며, 트래픽 증가 시 서비스 장애가 발생한다.
    • 관리의 복잡성 : 여러 대의 서버를 수동으로 관리하는 것은 복잡하고 많은 시간이 소요된다.
    • 장애 대응의 어려움 : 단일 서버에 장애가 발생하면 서비스 전체가 중단될 위험이 있다.
    클러스터링과 오케스트레이션의 필요성
    • 자원 효율성 : 클러스터링을 통해 여러 노드의 자원을 통합적으로 사용할 수 있다.
    • 확장성 : 클러스터에 새로운 노드를 쉽게 추가할 수 있다. (쉬운 수평 확장!)
    • 자동화된 관리 : 오케스트레이션 도구를 사용하여 자동으로 자원을 배포하고관리할 수 있다.
    • 고가용성 : 노드 중 일부에 장애가 발생해도 다른 노드가 이를 대체한다.

     

    도커 스웜

    도커에도 클러스터링과 오케스트레이션을 지원하는 스웜 모드가 있다. 스웜 모드는 여러 도커 엔진을 하나의 클러스터로 묶은 것으로, 매니저 노드와 워커 노드로 구성되어 있다.

    • 매니저 노드 : 워커 노드를 관리하는 서버
    • 워커 노드 : 컨테이너가 실행되는 서버
    💡 매니저 노드는 기본적으로 워커 노드 역할을 포함하고 있다. 클러스터는 최소 1개의 매니저 노드와 0개 이상의 워커 노드로 구성된다.
    🚨 매니저 노드의 절반 이상이 장애로 인해 정상적으로 동작하지 못할 경우, 매니저 노드가 복구될 때까지 운영을 중단한다.

    도커 스웜은 어플리케이션의 배포, 관리, 확장 등을 자동화하는 데사용된다. 어플리케이션을 여러 노드에 배포하고, 매니저 노드는 트래픽을 자동으로 분산시켜 노드 중 일부에 장애가 발생해도 서비스를 지속적으로 이용할 수 있도록 한다.

    🤔 매니저 노드와 워커 노드는 반드시 하나의 호스트 서버에 위치해야 할까?
    • 매니저 노드와 워커 노드는 여러 호스트에 분산되어 서로 다른 네트워크 상에 존재할 수 있다.
    • 여러 호스트에 걸쳐 있는 컨테이너들이 서로 통신할 수 있도록 오버레이 네트워크를 사용한다.
      1. 각 노드가 위치한 서버에 도커 노드를 설치한다.
      2. 매니저 노드를 초기화한다. `docker swarm init --advertise-addr [MANAGER_NODE_IP]`
      3. 각 워커 노드에서 토큰을 사용하여 클러스터에 조인한다. `docker swarm join --token [TOKEN] [MANAGER_NODE_IP]:2377`
      4. 매니저 노드에서 서비스를 생성한다. `docker service create --name [SERVICE_NAME] --replicas [CNT] [IMAGE_NAME]`

     

    쿠버네티스

    도커에서도 클러스터링과 오케스트레이션을 지원하는데 왜 쿠버네티스를 사용하는 걸까? 이는 쿠버네티스에서 도커 스웜보다 풍부한 기능과 유연성을 제공하기 때문이다.

    • 클러스터링 : 자원을 효율적으로 사용할 수 있고, 높은 가용성을 보장한다. 
    • 마이크로서비스 구조의 컨테이너 배포 : 어플리케이션 확장과 유지보수가 용이하다.
    • 장애 복구: 자가 치유 기능을 통해 높은 안정성과 가용성을 제공한다.
    • 퍼시스턴트 볼륨 : 데이터 손실을 방지하고 데이터의 일관성을 유지한다.
    • 스케줄링 : 작업을 자동으로 스케줄링하여 클러스터 내에서 자원을 효율적으로 분배할 수 있다.
    • 오토 스케일링 : 트래픽 증감에 따라 자동으로 인스턴스를 늘리거나 줄일 수 있다. 
    • 서비스 디스커버리 및 인그레스
      • 디스커버리 : 클러스터 내 서비스들이 서로를 자동으로 발견하고 통신할 수 있도록 지원한다.
      • 인그레스 : 외부 트래픽을 클러스터내 특정 서비스로 라우팅한다.

    도커 스웜과 마찬가지로, 쿠버네티스 클러스터는 마스터 노드와 워커 노드로 나뉜다.

    • 마스터 노드 : 클러스터 관리
      • API 서버 (kube-apiserver) : 쿠버네티스 API를 노출한다.
      • 컨트롤러 매니저 (kube-controller-manager) : 클러스터 상태를 모니터링하고, 특정 상태를 유지하기 위해 다른 여러  컨트롤러를 실행한다.
      • 스케줄러 (kube-scheduler) : 새로 생성된 파드를 적절한 워커 노드에 할당한다.
      • DNS 서버 (core DNS)
    • 워커 노드 : 어플리케이션 컨테이너 실행
      • kubelet : 컨테이너의 생명주기 관리하고 마스터와 워커 노드 간 통신을 지원한다.
      • 프록시 (kube-proxy)
      • 네트워크 플러그인 (calico, flannel 등) : 오버레이 네트워크를 구성한다.
    💡 마스터 노드도 워커 노드의 역할을 수행할 수 있다. 따라서 kubelet, 프록시, 네트워크 플러그인은 모든 노드에 존재한다.

     

    파드; 컨테이너를 다루는 기본 단위

    파드는 컨테이너 어플리케이션을 배포하기 위한 가장 작은 배포 단위로, 하나 이상의 컨테이너로 구성된 컨테이너 집합이다. 대부분의 경우 하나의 컨테이너를 포함한다. 만약 파드가 두 개 이상의 컨테이너를 포함하고 있다면 이 컨테이너들은 긴밀하게 협력해야 한다.

    apiVersion: v1
    kind: Pod
    metadata:
      name: nginx-pod
    spec:
      containers:
      - name: nginx-container
        image: nginx:latest
        ports:
        - containerPort: 80
          protocol: TCP

    위 코드에서 `containers` 섹션에 컨테이너가 하나밖에 없으므로, 하나의 컨테이너로 구성된 파드라고 볼 수 있다.   

    🚨 `containerPort`를 정의하긴 했지만 외부에서 접근할 수 있도록 노출된 상태는 아니다. 즉, `docker run`에서 `-p` 없이 실행한 것과같은 상태이다. 외부에서 파드에 접근하기 위해서는 서비스를 사용해야 한다. 
    🤔 파드가 도커 컨테이너보다 좋은 이유
    • 하나 이상의 컨테이너를 하나의 단위로 묶어서 관리할 수 있다.
    • 파드 내의 모든 컨테이너는 동일한 네트워크 네임스페이스*와 볼륨을 공유할 수 있다.
      • 도커 컨테이너는 여러 컨테이너 간에 네트워크를 공유하기 위해 네트워크를 생성하고 `docker network create` 연결하거나 `--network` , 링크 기능을 사용해야 `--link` 한다.
      • 파드 내 모든 컨테이너는 기본적으로 동일한 IP 주소와 네트워크 네임스페이스를 사용하여 별도의 설정이 필요 없다.
    • 파드의 수를 자동으로 조정하여 어플리케이션 부하에 맞게 스케일링할 수 있다.
    *네임스페이스
    • 리눅스 커널에서 제공하는 기능으로, 시스템 자원을 격리하여 독립적인 환경을 제공한다.
    • 네트워크 네임스페이스는 네트워크 자원을 격리하여 각 네임스페이스가 독립적으로 네트워크 인터페이스, IP 주소, 라우팅 테이블 등을 가질 수 있도록 한다.
    파드 배포 과정
    1. 파드를 위한 YAML 파일을 작성한다.
    2. YAML 파일을 클러스터에 적용한다.
    3. 마스터 노드는 해당 파드를 워커 노드 중 하나에 스케줄링한다.
    4. 선택된 워커 노드는 kubelet을 통해 파드를 생성하고 실행한다.

     

    레플리카셋; 일정 개수의 파드를 유지하는 컨트롤러

    레플리카셋은 파드의 복제본을 관리하고 유지한다. 특정 수의 파드가 항상 실행되도록 보장하여 어플리케이션의 가용성과 확장성을 보장한다.

    • 정해진 수의 동일한 파드가 항상 실행되도록 관리한다.
    • 파드를 사용할 수 없다면 다른 노드에서 파드를 다시 생성한다.

    여러 개의 파드를 직접 생성할 수도 있지 않을까? 하지만 이 방법은 파드에 더 이상 접근하지 못하게 되었을 때, 직접 파드를 삭제하고 다시 생성하지 않는 한 해당 파드는 다시 복구되지 않는다.

    apiVersion: apps/v1
    kind: ReplicaSet
    metadata:
      name: nginx-replicaset
    spec:
      replicas: 3
      selector:
        matchLabels:
          app: nginx-pods-label
       template:
         metadata:
           name: nginx-pod
           labels:
             app: nginx-pods-label
         spec:
           containers:
           - name: nginx-container
             image: nginx:latest
             ports:
             - containerPort: 80

    `spec.replicas`는 동일한 파드를 몇 개 유지할 것인지 설정한다. 위 코드의 경우, 파드 개수를 3으로 설정했기 때문에 레플리카셋은 3개의 파드를 유지한다. `spec.template`은 파드를 생성할 때 사용할 템플릿 (파드 스펙, 파드 템플릿) 을 정의한다.

     

    레플리카셋을 생성하면 파드가 생성되고, 삭제하면 파드 또한 삭제된다. 레플리카셋은 파드와 느슨하게 연결되어 있는데, 이는 라벨 셀렉터를 이용해 이루어진다. 라벨은 리소스의 부가적인 정보를 표현할 뿐만 아니라 서로 다른 오브젝트가 서로를 찾을 때 사용된다. 레플리카셋은 `spec.selector.matchLabels`를 통해 파드를 찾는다.

    🚨 라벨 셀렉터는 정규식을 지원하지 않는다. (Equality-based)
    레플리카셋 적용 과정
    1. 레플리카셋을 위한 YAML 파일을 작성한다.
    2. YAML 파일을 클러스터에 적용한다.
    3. 레플리카셋은 지정된 수의 파드가 실행 중인지 확인한다. 만약 부족하다면, 파드 템플릿을 사용하여 새로운 파드를 생성한다.
    4. 레플리카셋은 지속적으로 파드 상태를 모니터링하여 지정된 수의 파드를 유지한다.

     

    디플로이먼트; 레플리카셋/파드의 배포 관리

    디플로이먼트는 레플리카셋을 관리하는 상위 개념이다. 디플로이먼트를 생성하면 해당 디플로이먼트에 해당하는 레플리카셋도 함께 생성되어 파드와 레플리카셋을 직접 생성할 필요가 없다.

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: nginx-deployment
    spec:
      replicas: 3
      selector:
        matchLabels:
          app: nginx-pods-label
      template:
        metadata:
          name: nginx-pod
          labels:
            app: nginx-pods-label
        spec:
          containers:
          - name: nginx-container
            image: nginx:latest
            ports:
            - containerPort: 80

     

    그렇다면 왜 레플리카셋 대신 디플로이먼트를 사용하는 걸까? 디플로이먼트는 어플리케이션의 업데이트와 배포를 더욱 쉽게 만든다. 어플리케이션 업데이트 시 레플리카셋의 변경 사항을 리비전으로 저장하여 `--record` 롤백을 가능하게 한다. `--to-revision=[REVISION_NO]` 또한, 무중단 서비스를 위해 파드의 롤링 업데이트 전략을 지정할 수도 있다.

       

    서비스; 파드를 연결하고 외부에 노출

    이제 외부에서 파드에 접근하는 방법을 알아보자. 도커 컨테이너는 실행과 동시에 `-p`로 컨테이너를 외부로 노출할 수 있다. 하지만 디플로이먼트의 YAML 파일에는 파드의 어플리케이션이 사용할 내부 포트만 정의되어 있다. 이 포트를 외부로 노출하려면 서비스라는 별도의 오브젝트를 생성해야 한다.

     

    서비스는 클러스터 내에서 실행 중인 파드들을 네트워크를 통해 다른 파드와 연결해주는 역할을 한다. 주요 기능은 다음과 같다.

    • 여러 개의 파드에 대해 하나의 고유한 도메인 이름을 부여한다.
      • 클러스터 내에서 서비스 이름을 통해 해당 서비스에 연결된 모든 파드에 접근할 수 있다.
    • 여러 개의 파드에 접근할 때, 요청을 분산하는 로드 밸런서 기능을 수행한다.
    • 클라우드 플랫폼의 로드 밸런서, 클러스터 노드의 포트 등을 통해 파드를 외부로 노출한다.
      • LoadBalancer : 클라우드의 로드 밸랜서를 통해 외부 IP 주소를 할당받고, 이를 통해 외부에서 접근할 수 있다.
      • NodePort : 클러스터 노드의 특정 포트를 열어 외부 트래픽을 받는다. 

     

    서비스는 파드에 어떻게 접근할 것이냐에 따라 종류가 여러 개로 세분화된다.

    • ClusterIP : (default) 쿠버네티스 클러스터 내부에서만 파드에 접근할 수 있다.
    • NodePort : 파드에 접근할 수 있는 포트를 클러스터의 모든 노드에 동일하게 개방한다.
    • LoadBalancer : AWS나 GCP에서 지원하는 로드 밸런서를 사용해 외부 트래픽을 파드로 포워딩한다.
    ClusterIP NodePort LoadBalancer ExternalName*
    클러스터 내부 클러스터 내부 및 외부 클러스터 내부 및 외부 클러스터 외부
    *ExternalName : 외부 도메인을 참조하여 클러스터 내부의 파드들이 외부 도메인 이름을 통해 외부 서비스를 사용할 수 있도록 한다.
    🚨 ClusterIP 서비스는 클러스터 내부에서만 접근할 수 있다. 따라서 외부 사용자가 서비스를 통해 파드에 접근할 수 없다. 클러스터 외부로부터 접근을 허용하기 위해서는 NodePort나 LoadBalancer를 사용한다.
    🚨 실제 운영 환경에서 NodePort로 서비스를 외부에 제공하는 경우는 드물다. 대신 인그레스*를 사용한다.
    *인그레스 : 외부 요청을 클러스터 내부의 서비스로 라우팅하기 위한 규칙을 정의한다.

     

    apiVersion: v1
    kind: Service
    metadata:
      name: clusterip-service
    spec:
      ports:
        - name: nginx-port
          port: 8080
          targetPort: 80
       selector:
         app: nginx-pods-label
       type: ClusterIP

    `spec.selector`는 서비스에서 접근할 수 있는 파드를 결정한다. 위 코드에서는 `app=nginx-pods-label` 라벨을 가지는 파드들만 접근할 수 있다. `spec.ports.port`는 서비스에 접근할 때 사용하는 포트, `spec.ports.targetPort`는 파드가 내부적으로 사용하는 포트를 의미한다.

    ClusterIP 서비스의 동작 과정
    1. `[SERVICE_NAME]:[PORT]`나 `[SERVICE_IP]:[PORT]`로 접근한다.
    2. 서비스는 `selector`에 지정된 라벨을 가진 파드로 트래픽을 포워딩한다.

    참고자료

    댓글