[Kubernetes in Action] Services

2021. 10. 2. 19:26DevOps/Docker & Kubernetes

 

 

 

 

Overview

이전 포스팅에서 파드의 상태를 관리하고 항상 지정된 개수의 파드가 정상적으로 작동할 수 있도록 보장하는 Replication Controller, ReplicaSet에 대해서 살펴보았습니다. 이번 포스팅에서는 외부 / 내부의 클라이언트가 이렇게 생성된 여러 개의 파드와 안정적으로 연결할 수 있도록 도와주는 쿠버네티스 리소스인 "Service"에 대해서 살펴보려고 합니다.

 

 

 

Introducing Services

마이크로 서비스 아키텍쳐에서 대부분의 파드는 클러스터 내부의 다른 파드로부터, 혹은 클러스터 외부의 클라이언트로부터 HTTP Request를 받아 처리합니다. 파드 자체는 클러스터 내부에서 유일하게 식별될 수 있는 IP를 갖지만, 해당 파드의 IP를 통해 파드에 직접 접근하는 것은 다음과 같은 문제가 있습니다.

 

  • Pods are ephemeral
    이전 포스팅에서도 살펴보았듯, 파드는 서버가 죽는 등의 이유로 health check가 실패할 경우 종료되고, 새로운 파드가 생성됩니다. 새로운 파드는 종료된 파드와는 다른 IP를 가지고 있기 때문에 외부에서 Pod의 IP를 직접 가지고 있다면, 파드가 생성될 때마다 이를 변경해주어야 한다는 것을 의미합니다.

  • Kubernetes assigns an IP address to a pod "After" the pod has been scheduled to a node
    쿠버네티스는 파드가 워커 노드에 스케줄 된 이후에 해당 파드에 IP를 부여합니다. 따라서 앞으로 생성될 파드들의 IP를 미리 알 수 없으므로 이를 클라이언트 사이드에 미리 저장해둘 수 없음을 의미합니다.

  • Horizontal scaling means multiple pods may provide the same service
    ReplicaSet을 사용해서 동일한 컨테이너를 여러 개 생성하여 Horizontal Scaling을 할 경우에, 개별 파드의 IP를 모두 가지고 있는 것을 비효율적이며, 그렇게 할 수도 없습니다.

 

 

따라서 쿠버네티스에서는 여러 개의 파드(동일한 기능을 수행하는)를 하나의 엔트리 포인트로 접근할 수 있는 기능을 제공하며, 이것이 바로 서비스입니다. (앞으로 살펴볼 쿠버네티스 LoadBalancer는 쿠버네티스 서비스의 일종입니다.)

 

 

 

쿠버네티스는 서비스에 고유한 IP 주소를 부여하고 서비스가 존재하는 동안에는 이 IP주소가 바뀌지 않습니다. 클라이언트가 이 서비스로 요청을 보내면, 서비스는 selector를 통해 연결된 Pod로 요청을 흘려보냅니다. 

 

 

 

Creating Services

다음과 같이 서비스와 RC를 생성해보겠습니다. 서비스는 요청을 흘려보낼 파드를 "selector"를 통해 결정하기 때문에 service.spec.selector의 값과 파드의 셀렉터를 비교해서 파드를 찾습니다. (RC yaml의 pod spec.selector값을 확인해주세요)

 

 

apiVersion: v1
kind: Service
metadata:
  name: yeoul
spec:
  ports:
    - port: 80
      targetPort: 8080
  selector:
    app: yeoul
apiVersion: v1
kind: ReplicationController
metadata:
  name: yeoul
spec:
  replicas: 3
  selector:
    app: yeoul
  template:
    metadata:
      labels:
        app: yeoul
    spec:
      containers:
        - name: yeoul
          image: luksa/kubia
          ports:
            - containerPort: 8080

 

 

아래와 같이 정상적으로 파드와 RC, Service가 생성된 것을 확인할 수 있습니다. 별도의 설정이 없다면 Service의 External IP가 설정되지 않았으므로 쿠버네티스 클러스터 외부에서는 서비스에 요청을 보낼 수 없습니다. 대신에 직접 생성된 파드 하나로 들어가서 해당 파드에서 서비스에 요청을 보내면, 아래와 같이 정상적으로 생성된 파드 중 하나로 요청이 제대로 라우팅 되는 것을 확인할 수 있습니다.

 

 

 

kubectl exec pod/yeoul-d2227 -- curl -s http://10.105.173.75

 

 

 

 

서비스는 별도의 설정이 없다면(바로 아래에서 세션관련 설정에 대해 알아봅니다.) 요청을 서비스에 연결된 파드에 임의로 흘려보내 줍니다. 따라서 서비스에 여러 번 요청을 보내면, 요청을 보낼 때마다 다른 파드로 요청이 흘러가는 것을 확인할 수 있습니다.

 

the service proxy normally forwards each connection to a randomly selected backing pod

 

 

 

 

 

Configuring Session Affinity on the Service

만약 특정 IP로부터 온 요청을 항상 동일한 파드로 흘려보내게 하고 싶다면 Service의 스펙에 다음과 같이 "sessionAffinity" 옵션을 "ClientIP" (default = "None")로 추가해주면 됩니다. 그러면 쿠버네티스 서비스는 요청 클라이언트의 IP를 식별하고, 동일한 클라이언트로부터 온 요청은 동일한 파드로 흘려보내 줍니다.

 

 

sessionAffinity 옵션에는 "cookie-based"관련 옵션이 없는데, 이는 내부적으로 Service는 TCP / UDP 패킷을 처리하고 그 위의 레이어의 정보는 처리하지 않기 때문입니다. 쿠키는 HTTP 레이어의 정보이므로 쿠버네티스 서버에서는 쿠키 관련 sessionAffinity 정보를 처리하지 않습니다.

 

 

spec:
  sessionAffinity: ClientIP

 

 

 

Discovering Services

서비스를 생성하면 쿠버네티스는 서비스의 Port와 IP 주소를 생성합니다. 하지만 파드가 서비스에 접근하려고 할 때, 이 파드는 서비스의 IP 주소와 Port 정보를 어떻게 아는 것일까요?  쿠버네티스에서는 시스템 내에 존재하는 서비스들에 대한 환경 변수를 파드 생성 시점에 초기화하는 방식을  통해 서비스에 대한 정보들을 넣어줍니다. 

 

 

다음과 같이 파드의 환경에 접근해서 환경변수의 목록을 확인해보면 아까 생성한 YEOUL_SERVICE에 대한 정보들이 이미 저장되어 있는 것을 알 수 있습니다 파드는 이 정보를 통해 서비스에 접근하고 요청을 보낼 수 있는 것입니다.

 

 

kubectl exec yeoul-cnrbs env

 

 

 

Discovering Services Through DNS

쿠버네티스는 실행 시에 "kube-system"이라는 네임스페이스를 제공합니다. 이 네임스페이스에는 "kube-dns"라는 서비스가 있는데 이 서비스는 이름 그대로 DNS resolve를 수행하는 역할을 합니다. 

 

 

 

 

kubectl config set-context --current --namespace=kube-system

 

따라서 이 파드 내의 DNS 서버를 통해 파드 내의 프로세스에서 수행되는 DNS 쿼리가 처리되게 됩니다. 관련된 DNS 정보는 파드 내에 /etc/resolve.conf 파일 내에 저장해 두고 있고, 각각의 서비스는 위의 DNS 서버에서 엔트리 하나씩을 가지므로 서비스를 다음과 같이 요청할 수 있습니다.

 

 

 

 

 

Connecting to services Living outside the Cluster

쿠버네티스 서비스는 파드와 직접 커넥션을 맺고 있는 리소스가 아닙니다. 실제로 서비스는 요청을 흘려보낼 파드에 대한 Endpoint만 가지고 있고, 이를 통해 요청을 흘려보내 줍니다. (Endpoint는 쿠버네티스 리소스의 하나입니다.) kubectl describe 명령어를 통해 서비스의 정보를 보면 다음과 같이 연결된 3개의 파드에 대한 Endpoint가 각각 연결되게 됩니다. 

 

 

 

쿠버네티스 서비스의 selector에 매칭 되는 파드가 하나도 없는 경우에는 Endpoint 자체가 생성되지 않으며, Endpoint를 yaml 파일에 명시함으로써 외부의 리소스와도 연결할 수 있습니다. 다음과 같이 selector가 없는 Service를 만들고, name이 동일한(name이 동일하지 않으면 Endpoint가 Service와 연결되지 않습니다.) Endpoint를 만들어 보겠습니다. 이 Endpoint에는 외부 IP (클러스터 내부에서 사용하는 가상의 IP가 아닌 실제 IP) 2개가 명시되어 있습니다.

 

 

apiVersion: v1
kind: Service
metadata:
  name: yeoul-empty
spec:
  ports:
    - port: 80
      targetPort: 8080​

 

apiVersion: v1
kind: Endpoints
metadata:
  name: yeoul-empty
subsets:
  - addresses:
      - ip: 11.22.33.44
      - ip: 22.33.44.55
    ports:
      - port: 80

 

 

 

두 yaml 파일을 적용하고 해당 서비스를 확인해보면 서비스에 위에서 명시한 External IP 2개가 Endpoint로 잘 매핑된 것을 확인할 수 있습니다. 따라서 이 서비스에 요청을 보내면 서비스가 요청을 External IP 2개로 흘려보내 주게 됩니다. 즉 클러스터 내부에서 외부로 요청을 보낼 수 있게 되는 것입니다.

 

 

 

 

 

 

Exposing Services to External Clients

실제로 마이크로 서비스를 구성하고 운영하기 위해서는 클러스터 외부의 클라이언트가 해당 서비스에 접근하여 요청하는 방법을 제공해야 합니다. 쿠버네티스에서는 이와 같이 내부 클러스터 서비스를 외부에 노출하기 위해 다음과 같은 3가지 방법을 제공합니다.

 

  • Setting the Service Type to NodePort
  • Setting the Service Type to LoadBalancer
  • Creating an Ingress resource

 

 

Using a NodePort Service

NodePort 서비스를 생성하면 쿠버네티스는 모든 워커 노드에 동일한 포트를 개방합니다. 워커 노드의 특정 포트 자체가 외부에 개방되는 것이므로 외부에서 워커 노드의 IP를 사용해서 해당 포트에 직접 접근할 수 있으며, 해당 포트에 바인딩된 서비스가 요청을 파드로 흘려보내 줍니다. 

 

 

apiVersion: v1
kind: Service
metadata:
  name: yeoul-nodeport
spec:
  type: NodePort
  ports:
    - port: 80
      targetPort: 8080
      nodePort: 30123
  selector:
    app: yeoul

 

 

 

Exposing a service through External Load Balancer

앞서 설명한 NodePort도 외부에 서비스를 노출할 수 있는 방법이지만, 노드 자체를 개방하는 방법이기 때문에 보안 설정을 위해 방화벽 설정 등의 추가적인 단계를 더 거쳐야 하는 불편함이 있습니다. Load Balancer를 사용하면 이러한 불편함을 없앨 수 있습니다.

 

 

Load Balancer는 그 자체로 Unique한 IP를 가지기 때문에 워커 노드의 IP를 알 필요 없이 Load Balancer의 IP로 바로 접근할 수 있습니다. 쿠버네티스는 AWS, GCP 같은 Cloud Provider에서 제공하는 Load Balancer를 그대로 프로비저닝 해서 사용하기 때문에 그냥 yaml파일 안에 서비스 타입을 LoadBalancer로 설정해주기만 하면 Load Balancer를 사용할 수 있습니다. (minikube의 경우 load balancer를 사용하기 위해서는 다른 터미널에 minikube tunnel 커맨드를 사용해서 활성화해주어야 합니다. 자세한 내용은 아래 Reference를 참고해주세요)

 

 

 

apiVersion: v1
kind: Service
metadata:
  name: yeoul-loadbalancer
spec:
  type: LoadBalancer
  ports:
    - port: 80
      targetPort: 8080
  selector:
    app: yeoul

 

 

 

 

Understanding the peculiarities of External Connections

위에서 설명했던 NodePort 개념을 사용하면 다음과 같이 불필요한 Network Hop을 발생시키는 케이스가 생기게 됩니다. 즉, 외부에서 특정 노드로 요청을 보냈는데, 서비스가 해당 노드의 파드가 아닌 다른 노드의 파드로 요청을 보내는 것입니다. 즉 Node 1, Node 2 안에 각각 Pod 1, Pod 2가 있는데 Node 1을 통해 접근했음에도 서비스가 Node 1의 Pod 1이 아닌 Node 2의 Pod 2로 요청을 보내는 경우입니다.

 

 

이런 경우 "externalTrafficPolicy: Local"로 서비스 스펙을 설정함으로서 요청이 들어온 노드의 파드로 요청을 흘려보낼 수 있습니다. 이렇게 설정하면 로드 밸런서가 모든 "파드"에 균등하게 요청을 분배하는 것이 아닌 모든 "노드"에 균등하게 요청을 분배하게 되고, 따라서 워커 노드 안에 들어있는 파드의 개수가 다를 경우 특정 노드의 파드가 다른 노드의 파드보다 더 많은 요청을 받게 될 수 있습니다. 아래의 그림을 참고해주세요

 

 

 

 

Exposing Services Externally Through an Ingress Resource

Ingress는 접근, 입장 정도의 의미를 가지고 있으며, 로드 밸런서와는 다르게 하나의 Ingress를 가지고 여러 서비스로 요청을 흘려보내줄 수 있습니다. Ingress는 Service와는 다르게 TCP레이어가 아닌 HTTP 레이어 위에서 동작하기 때문에 "cookie-based" sessionAffinify를 사용할 수 있다는 장점도 있습니다. 

 

 

예를 들어 아래와 같이 서로 다른 4개의 서비스를 외부에 노출하기 위해서는 4개의 로드밸런서를 생성해야 했지만, Ingress를 사용하면 단 하나의 Ingress를 가지고 모든 서비스를 외부에 노출시킬 수 있습니다. 

 

 

Understanding How Ingresses Work

Ingress가 동작하는 방식은 다음과 같습니다.

 

  1. Client가 bar.example.com 으로 요청을 보내기 전에 DNS lookup을 통해 Ingress의 IP를 찾아냅니다.
  2. 해당 IP를 가지고 "HOST: bar.example.com" 헤더를 추가하여 Ingress에 요청을 보냅니다.
  3. Ingress는 Host 헤더 값을 통해 특정 서비스에 해당 요청을 매핑해 줍니다.
  4. 서비스는 가지고 있는 Endpoint를 통해 요청을 특정 파드로 넘깁니다

 

 

 

 

 

 

Signaling when a Pod is Ready to Accept Connections

이전 포스팅에서 파드의 "livenessProbe"에 대해서 살펴보았습니다. 주기적으로 health check를 통해 파드가 정상적으로 작동하는지를 확인하고 만약 health check가 실패할 경우 해당 파드를 제거하고 다시 생성할 수 있도록 도움을 줍니다. 비슷한 개념으로 "readinessProbe"라는 개념이 있습니다. 이는 해당 파드가 "요청을 받을 준비가 되었는지"를 표시하기 위한 개념으로 readinessProbe가 성공하게 되면 해당 파드는 서비스에 포함되어 네트워크 트래픽을 받게 됩니다.

 

 

livenessProbe와의 차이점은 check가 실패하는 경우, 파드를 삭제하고 재생성하는 것이 아니라 그냥 서비스에 포함하지 않는 것으로 끝난다는 점입니다. 말 그대로 "요청을 받을 준비가 되었는가"를 판단하는 로직이기 때문에 실패했다고 파드를 제거하지는 않습니다. check가 성공하고 요청을 받을 준비가 되었다면 해당 파드의 내부 IP가 서비스의 Endpoint에 포함되게 됩니다. 

 

 

 

apiVersion: v1
kind: ReplicationController
...
spec:
  ...
  template:
    ...
    spec:
      containers:
        name: yeoul 
        image: luksa/kubia
        readinessProbe:
          command:
          - ls
          - /var/ready

 

 

 

 

Reference

https://minikube.sigs.k8s.io/docs/handbook/accessing/#using-minikube-tunnel

 

Accessing apps

How to access applications running within minikube

minikube.sigs.k8s.io

 

 

 

반응형