본문 바로가기
Computer Science/Kubernetes

Chapter 5. Services: enabling clients to discover and talk to pods

by zxcvber 2021. 4. 7.

Using kubectl exec to test out a connection to the service by running curl in one of the pods. (출처: https://livebook.manning.com/book/kubernetes-in-action/chapter-5/52)

많은 앱들이 request (요청) 을 받아 서비스를 제공하는 형태인데, 이런 요청을 보내려면 IP 주소를 알아야 한다. 한편 Kubernetes 를 사용하게 되면 pod 의 IP 주소를 알아야 하는데, Kubernetes 의 pod 들은 굉장히 동적이므로 이들의 IP 주소를 알아낼 방법이 필요하다.

Pod 들은 스케쥴링 되고 스케일링 되기 때문에 IP 주소가 자주 바뀔 수 있으며, pod 가 시작된 후 IP 주소가 할당될 뿐만 아니라, 하나의 서비스를 위해 pod 를 여러 개 사용하는 경우 하나의 IP 를 사용하여 여러 개의 pod 에 접근할 방법이 필요하다. (쿠버네티스를 사용하지 않으면 고정 IP 등을 사용하거나 따로 설정된 주소로 요청이 가도록 했을 것이다)

5.1 Services 소개

Service 를 이용하면 같은 일을 하는 여러 개의 pod 에 하나의 entrypoint 를 제공할 수 있다.

Service 는 IP 와 포트가 설정되어 있고 (service 가 살아있는 동안) 고정되어 있다. 자신의 주소로 요청이 들어오면 연결된 pod 중 하나로 요청을 보내준다. (로드 밸런싱도 가능)

5.1.1 Service 만들기

Service 에 포함되는 pod 를 정의하기 위해서는 label selector 를 사용한다.

apiVersion: v1
kind: Service
metadata:
  name: sung
spec:
  ports:
  - port: 80
    targetPort: 8080
  selector:
    app: kubia

Service 가 실행했을 때 할당되는 IP 주소의 80 포트로 들어오는 요청은 app=kubia label 을 가진 pod 의 8080 포트로 요청이 전달된다.

kubectl create 로 service 를 생성한 후, kubectl get svc 를 하면 service 목록을 확인할 수 있다. Column 중에 CLUSTER-IP 가 있는데 이는 클러스터 내부에서 사용하는 IP 주소이다. (EXTERNAL-IP 는 외부에서 사용하는 주소인데 일단은 <none> 이다.)

원래 service 의 주 목적은 클러스터 내의 pod 들을 다른 pod 들에게 보여주는 것이므로 cluster IP 가 존재한다. (프론트, 백 각각 service 로 만들어 두면 프론트에서 백엔드 service 에 접근할 수 있어야 한다) 하지만 주로 클러스터 외부로 expose 하는 경우가 더 많을 것이다.

Session affinity

4장에서 사용한 ReplicationController 를 생성하고 service 를 만든 뒤 service 의 주소로 curl 을 날려보면 항상 같은 pod 가 요청을 처리하지는 않는다는 것을 확인할 수 있다. 만약에 같은 client 의 요청은 같은 pod 에서 처리하고 싶다면 .spec.sessionAffinity 를 설정하면 된다.

Service 의 sessionAffinityNoneClientIP 만 지정할 수 있다. ClientIP 로 지정하게 되면 같은 client 의 요청은 하나의 pod 에서 무조건 처리하게 된다.

Cookie 를 사용한 affinity 설정은 불가능하다. Service 가 애초에 TCP/UDP 레벨 (transport layer)에 있어서 Cookie 가 있는 HTTP 레벨 (application layer) 과 다르다.

여러 개의 포트 개방

여러 개의 포트를 개방하여 pod 의 포트로 redirect 시킬 수 있다. 다만 각 포트에 해당하는 이름을 설정해줘야 한다.

...
spec:
  ports:
  - name: http # 이름을 지정해줘야 한다
    port: 80
    targetPort: 8080
  - name: https 
    port: 443
    targetPort: 8443
  selector:
    app: kubia

또한 label selector 는 포트마다 다르게 설정할 수 없다. 포트마다 다른 label selector 를 사용하고 싶다면 service 를 여러 개 만들어야 한다.

포트에 이름 붙이기

어플리케이션이 복잡해지면 포트를 변경할 일이 생길 수도 있다. 포트 번호를 단순히 숫자로 관리하면 나중에 pod 레벨에서 port 가 변경되었을 때 이를 참조하는 모든 쿠버네티스 오브젝트의 spec 에서 포트 번호를 변경해야 할 수도 있다. 그래서 pod 정의시 포트에 이름을 붙일 수 있다.

kind: Pod
spec:
  containers:
  - name: kubia
    ports:
    - name: http # 포트에 이름 붙이기
      containerPort: 8080
    - name: https
      containerPort: 8443

Service 의 YAML 파일에서는 아래와 같이 사용하면 된다.

kind: Service
spec:
  ports:
  - name: http
    port: 80
    targetPort: http
  - name: https
    port: 443
    targetPort: https

Service 는 label selector 에 match 된 pod 중 임의의 한 pod 에 요청을 보낼텐데, 그 pod 의 정의에 포트 이름이 정의되어 있으면 그 포트 번호로 요청을 보내는 듯하다.

그러므로 이런 것도 가능하다. 예전에 만든 pod 에는 8080 포트에 이름 http 가 있었는데, 새로 만든 pod 에는 80 포트에 http 를 붙여놓았다면, (같은 일을 하는 pod 라고 하자) service 에서는 http 라는 이름만 이용해서 pod 에게 요청을 전달하므로 실제 포트 번호는 다르게 설정할 수도 있다.

5.1.2 Discovering services

Service 또한 pod 처럼 생성이 되어야 IP 주소가 할당되므로, pod 와 같은 문제를 갖고있다. Service 를 생성하고 그 IP 를 pod 에 하드코딩 하는 것도 바람직하지는 않아 보인다.

service 를 위한 service

특정 service 가 필요한 pod 들에게 service 의 IP 주소를 알려줄 방법이 필요하다.

환경 변수 사용

쿠버네티스는 pod 가 생성될 때 현재 존재하고 있는 각 service 를 가리키는 환경 변수를 설정한다.

그래서 service 를 먼저 만들고 이를 필요로 하는 pod 를 생성하면 환경 변수에서 service 의 IP 주소를 알 수 있다.

단, 이렇게 하면 의존성이 생기고, 쿠버네티스 리소스를 띄워야 하는 순서가 생겨버린다.

DNS, FQDN

쿠버네티스에도 DNS 서버가 있어서, 각 service 는 내부 DNS 서버에 등록된다. 그래서 pod 가 만약 service 의 이름을 알면 FQDN 을 이용해서 접근할 수 있게 된다.

FQDN 은 대략 이런 모양이다.

<service_name>.<namespace>.svc.cluster.local

단, 이렇게 하더라도 service 가 listen 하고있는 포트는 알아야 한다. (이건 IP 주소 알아내는 것보다는 쉬울 듯)

만약 namespace 가 같다면, service 의 이름만 가지고서도 접근이 가능하다!

Service IP 로 ping 이 불가능한 이유

curl 은 되지만 ping 은 안되는 이유는 cluster IP 가 virtual IP 라서 포트와 결합되었을 때 의미를 갖기 때문이다.

정확히는, ping 명령어는 ICMP 위에서 동작한다. 그런데 service 의 클러스터 내부에서 특정 포트만 사용하기 때문에 ICMP request 에 응답하지 않으므로 ping 이 불가능하다.

5.2 클러스터 외부의 service 에 접속하기

5.2.1 Service endpoints

Service 와 pod 은 직접 연결되지 않고 사이에 endpoints 라는 리소스가 있다. kubectl describe svc <SVC_NAME> 을 실행해보면 Endpoints: ... 라면서 IP 주소와 포트가 나열되어있는 것을 확인할 수 있다.

Pod selector 가 YAML 파일에 정의되어 있기는 하지만, 실제로 service 가 요청을 처리할 때는 pod selector 를 사용하지 않고 이 endpoints 에 있는 값들을 사용한다. Selector 는 endpoints 의 값들을 만들 때 사용한다.

그럼 endpoints 의 값들은 언제 갱신하는데? Pod 이 생길 때마다? -> 계속 읽어보니 readiness probe 가 성공했을 때 추가된다고 한다.

5.2.2 Service endpoints 직접 설정하기

Pod selector 가 없는 service 를 만들면 endpoints 가 설정되지 않는다.

Endpoints 리소스를 직접 만들어서 service 에 갖다 붙일 수 있다. 다만 만들어진 endpoints 리소스의 이름은 service 의 이름과 동일해야 한다.

이 방법을 이용하면 외부의 서버 등에 연결할 수 있는 service 를 하나 만들어두고 pod 들이 외부 서버에 접속할 때 해당 service 로 요청을 보내도록 할 수 있다. (로드 밸런싱은 덤)

Label selector 를 지정하면 endpoints 가 자동으로 관리되고, label selector 를 제거하면 endpoints 의 갱신은 멈춘다.

5.2.3 외부의 서비스 alias 설정

외부 서비스의 FQDN 을 이용해서 접근할 수 있다. Service 오브젝트를 생성할 때 .spec.typeExternalName 으로 설정하면 된다. 이렇게 하면 외부 서비스의 FQDN 을 직접 이용하지 않고 service 를 경유하여 사용할 수 있게 된다. 나중에 외부 서비스를 교체할 일이 생길 때 훨씬 수월하게 교체할 수 있게 된다.

ExternalName service 들은 DNS 레벨에서 구현되어 있어서 CNAME record 가 생성된다. 이 service 에 접속을 시도하는 client 들은 외부 서비스에 직접 접속하며, service 에는 IP 가 할당 되지 않는다.

5.3 Service 를 외부에 공개하기

프론트엔드의 경우 외부로 공개할 필요가 있다. 공개하는 방법에는 세 가지 방법이 있다.

  • Service 타입 NodePort 로 설정하기: 클러스터의 노드가 한 포트를 개방한다. 개방된 포트로 들어오는 요청은 해당 service 로 redirect 된다. 특정 노드에 구애받지 않고, 모든 노드의 해당 포트로 들어오는 요청이 redirect 된다.
  • Service 타입 LoadBalancer 로 설정하기: 해당 service 를 위한 load balancer 의 IP 주소를 통해서 service 에 접근할 수 있도록 한다. (Cloud infrastructure 에서 관리)
  • Ingress 리소스 만들기: HTTP level 에서 동작한다. 5.4 에서 설명한다.

5.3.1 NodePort service

NodePort service 를 생성하면 모든 노드의 한 포트가 reserve 된다. 해당 포트로 들어오는 요청은 모두 service 로 연결된다.

일반적인 service 와 크게 다르지 않지만, cluster IP 뿐만 아니라 임의의 노드 IP 와 포트를 통해서도 접근이 가능하다.

NodePort service 생성

...
spec:
  type: NodePort
  ports:
  - port: 80
    targetPort: 8080
    nodePort: 30123 # 노드의 포트를 개방하도록 설정

개방할 포트를 안적으면 random 하게 선택되며, GKE 의 경우 방화벽 설정을 해야한다.

모든 노드의 IP 주소로부터 접근이 가능하지만, 만약 어떤 client 가 무조건 한 노드의 IP 로만 접근하는데 해당 노드가 죽는 경우에는 접근이 불가능해진다. 따라서 load balancer 를 사용하는 방법이 더 바람직하다.

5.3.2 Exposing a service through an external load balancer

Service 타입을 LoadBalancer 로 설정하면 된다. 다만 cloud infrastructure 단에서 load balancer 를 지원해줘야 한다. 만약 지원해주지 않는다면 NodePort service 와 동일하게 동작한다. (extension 이기 때문)

LoadBalancer service 를 생성하게 되면 해당 service 에 external IP 가 설정되며, 해당 주소로 접근할 수 있다.

LoadBalancer service 를 생성할 때도 NodePort service 가 그랬듯이 모든 노드에서 한 포트를 개방하게 되며, 로드 밸런싱을 거쳐 적절한 노드로 요청을 redirect 해준다. 또한 노드에서 한 포트가 개방되는 셈이므로, kubectl describe 를 이용하여 개방된 포트를 확인하고 방화벽 설정을 고쳐주면 NodePort service 처럼 노드의 IP 주소와 포트를 이용해 접근할 수 있다.

5.3.3 Understanding the peculiarities of external connections

Avoiding additional network hops

NodePort service 를 이용하게 되면 요청을 받은 노드에 요청을 처리해줄 pod 이 존재하지 않을 수도 있다. 혹은 임의로 선택된 pod 가 해당 노드에 존재하지 않을 수도 있다. 이 때 해당 pod 이 존재하는 노드로 가야하기 때문에 추가 delay 가 생긴다.

.spec.externalTrafficPolicylocal 로 설정하면 요청을 받은 노드 안에 있는 pod 로만 redirect 된다. 만약에 그런 pod 가 없으면 대기한다. 그러므로 쓰기 전에 노드에 pod 가 있음을 확신할 수 있어야 한다.

위 옵션을 사용하면 더 이상 요청이 pod 별로 균등하게 분배되지 않는다.

Non-preservation of the client's IP

NodePort service 를 통해 외부에서 접근하게 되면 SNAT 로 인해 source IP 가 변경된다. (내부에서는 무관) 그런데 만약 어플리케이션이 source IP 를 필요로 한다면 문제가 될 수도 있다.

5.4 Ingress resource

Ingress 가 필요한 이유

LoadBalancer service 는 각 service 마다 load balancer 를 생성하고, 공개 IP 주소가 필요하지만, Ingress 는 여러 개의 service 를 관리하는데도 하나의 IP 주소만 있어도 된다. HTTP 요청을 받을 때, host 와 path 를 보고 어느 service 로 redirect 해야하는지 알 수 있다.

또한 application layer 에서 동작하기 때문에 service 가 제공하지 않는 기능들을 사용할 수 있다.

(IP 주소 하나 쓰는 것만으로는 조금 부족해 보이는데 어떤 기능들이 있는지 확인해 봐야겠다.)

더불어 Ingress 가 동작하려면 Ingress controller 가 동작하고 있어야 한다.

5.4.1 Ingress 리소스 생성

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: sung
spec:
  rules:
  - host: sung.example.com # 이 주소로 들어오는 요청은 서비스로 redirect
    http:
      paths:
      - path: /
        backend:
          serviceName: sung
          servicePort: 80

Note. 위 처럼 생성하면 warning 이 발생한다.

Warning: extensions/v1beta1 Ingress is deprecated in v1.14+, unavailable in v1.22+; use networking.k8s.io/v1 Ingress

5.4.2 Ingress 를 이용하여 service 접속

Domain name resolution 이 잘 되어야 한다! kubectl get ingresses 로 IP 를 확인할 수 있다. Ingress 에 입력한 host 값이 해당 IP 로 resolve 될 수 있도록 직접 설정해줘야 한다. /etc/hosts 를 쓰거나, DNS 서버를 직접 사용하거나...

How does it work?

  1. Client 가 sung.example.com 의 IP 주소를 DNS 에서 찾아본다.
  2. 받은 IP 주소에 HTTP 요청을 보낼 때 Host: sung.example.com 헤더를 넣어서 보낸다.
  3. Ingress controller 는 어느 service 에 대한 요청인지 파악한다.
  4. 해당 서비스의 endpoints 중 하나의 주소로 요청을 보낸다.

유의하여 볼 점은 service 에게 요청을 전달하지 않고 직접 pod 를 선택했다는 점이다.

5.4.3 하나의 Ingress 에 여러 service 연결하기

Mulitple paths

...
- host: sung.example.com
  http:
    paths:
    - path: /sung # sung.example.com/sung
      backend:
        serviceName: sung
        servicePort: 80
    - path: /foo # sung.example.com/foo
      backend:
        serviceName: bar
        servicePort: 80

각 path 별로 다른 service 를 사용하도록 설정할 수 있다.

Multiple hosts

host 를 여러 개 등록하면 Host: ... 헤더 값에 따라 다른 service 에 접근할 수 있다. DNS 에 host 값을 등록하는 것을 잊으면 안 된다.

5.4.4 Configuring Ingress to handle TLS traffic

HTTPS 요청을 처리하는 방법에 대해서 알아본다.

TLS certificate for the Ingress

Client 와 controller 사이의 통신은 암호화 되어있지만, controller 와 pod 사이의 통신은 암호화 되어있지 않다. 그러므로 pod 에서 돌아가는 앱은 TLS 를 지원할 필요는 없다.

인증서와 private key 를 생성하여 Ingress 에 넣어주어야 한다. 원래는 Secret 이라는 쿠버네티스 리소스에 저장되지만 7장에서 자세히 살펴본다.

$ openssl genrsa -out tls.key 2048
$ openssl req -new -x509 -key tls.key -out tls.cert -days 360 -subj CN=sung.example.com

# Create Secret
$ kubectl create secret tls tls-secret --cert=tls.cert --key=tls.key

이제 YAML 파일을 수정한다.

...
spec:
  tls:
  - hosts:
    - sung.example.com
    secretName: tls-secret
  rules:
...

5.5 연결을 받을 준비가 되었을 때 신호 보내기

Service 나 Ingress 를 사용하면 pod 가 생성되자마자 요청이 전달될 수 있다. (label 만 검사하므로) 한편 startup 이 오래걸리는 어플리케이션의 경우 아직 pod 는 준비가 안되었는데 요청을 받게 되어 요청을 정상적으로 처리 못하게 될 수 있다.

Pod 가 요청을 처리할 준비가 되었음을 확인할 방법이 필요하다.

5.5.1 Readiness probes

Readiness probe 는 주기적으로 실행되며, pod 가 client request 를 받을 준비가 되었는지 확인한다.

종류는 liveness probe 와 마찬가지로 세 종류가 있다.

  • HTTP GET probe: 컨테이너 IP 주소에 GET 요청 보내보기
    • 응답 코드가 2xx, 3xx 이면 OK
    • 이외의 에러 코드나 응답이 없으면 실패로 간주
  • TCP socket probe: 컨테이너의 정해진 포트에 TCP 연결을 시도해서 성공하면 준비된 것으로 판단
  • Exec probe: 컨테이너 안에서 임의의 명령을 실행하고 exit code 가 0 이면 준비된 것으로 판단

Understanding the operation of readiness probes

  • 첫 readiness probe 를 실행하기 전 얼만큼 기다릴지 미리 설정할 수 있다. 그 이후에는 주기적으로 실행한다.
  • Ready 상태가 아니면 service 의 endpoints 에서 제거되고, 준비가 되면 추가된다.
  • Liveness probe 는 실패하면 컨테이너를 재시작하지만, readiness probe 는 그렇지 않다.

Pod 이 죽을 때 어떤 방식으로 endpoints 를 업데이트 하는지? -> 이게 아니고 readiness probe 가 실패해서 endpoints 가 업데이트 되는 듯 하다. + 삭제시 쿠버네티스가 알아서 업데이트 해준다.

아무튼, readiness probe 는 client 들이 healthy pod 에만 요청을 할 수 있도록 관리해준다.

5.5.2 Pod 에 readiness probe 추가하기

ReplicationController's pod template 를 수정하여 readiness probe 를 추가한다. 컨테이너별로 readiness probe 의 동작을 다르게 정할 수 있다.

ReplicationController 의 pod template 을 수정하면 현재 존재하는 pod 에는 영향이 없으므로, 존재하는 pod 들은 삭제하고 새롭게 pod 를 띄우거나, ready 상태가 될 수 있도록 적절한 동작을 취해줘야 할 수도 있다.

5.5.3 Readiness probe 가 해야할 일

  • 항상 readiness probe 를 사용할 것

사용하지 않으면, pod 의 시작과 동시에 service endpoints 에 추가되므로, 요청을 처리하지 못할 것이다.

  • Don't include pod shutdown logic

Shutdown 이 시작되었음을 감지하고 readiness probe 가 실패하도록 로직을 짤 필요는 없다. Pod 가 삭제되면 쿠버네티스가 자동으로 service endpoints 에서 제거한다.

5.6 Headless service for discovering individual pods

Client 가 모든 pod 에 접근해야 한다면? (어떤 경우에 이러한가? - 아래 Discussion 참고)

DNS lookup 을 이용할 수 있다! 보통 service 를 위해 DNS lookup 을 하면 하나의 IP (service IP) 를 돌려주지만, service 에 cluster IP 가 필요없다고 설정하면, endpoints 에 있는 IP 들을 DNS A 레코드로 돌려준다.

5.6.1 Headless service 생성

.spec.clusterIP: None 으로 설정하면 된다.

5.6.2 Discovering pods through DNS

nslookup 명령어로 확인하면 된다!

YAML 없이 pod 띄우기

$ kubectl run <POD_NAME> --image=<IMAGE_NAME> --generator=run-pod/v1

진짜로 pod 만 만들어준다. ReplicationController, service 이런거 없이 만들어 준다.

(아 이거 왜 이제 알려줌 ㅋㅋ)

5.6.3 Discovering all pods - even those that aren't ready

Service 에서 publishNotReadyAddresses=True 로 설정하면 된다.

5.7 Troubleshooting

순서대로 해보면 좋을 것이다!

  • Service cluster IP 를 사용하는 경우 클러스터 내에서 접속하고 있는지 확인하자.
  • Service IP 에는 ping 이 동작하지 않는다.
  • Readiness probe 를 설정했다면 성공하는지 꼭 확인해야 한다. 그렇지 않으면 pod 가 service 의 endpoints 에 등록되지 않는다.
  • Pod 가 service 의 endpoints 에 등록되었는지 확인하려면 kubectl get endpoints.
  • FQDN 으로 접속이 안되면, cluster IP 를 사용해서 되는지 확인해본다.
  • Service 의 타겟 포트가 아니고 expose 한 포트로 접속하고 있는지 확인한다.
  • Pod IP, 포트로 직접 접속하여 요청을 받는지 확인한다.
  • 마지막으로, 앱이 localhost 에 바인딩 하지 않았는지 확인한다.

11장에서 service 가 어떻게 구현되었는지 공부하면 디버깅이 더 쉬울 것이라고 한다 ...


Discussion & Additional Topics

When would you want to set sessionAffinity to ClientIP ?

  • 좋은 방법은 아니겠지만, session 정보를 저장해야 하는 경우가 그럴 것이다. 예를 들면 pod A, B 가 실행 중인데 처음에 A 로 접속해서 로그인했다고 하자. 그러면 다음 요청을 보낼 때 A 로 가게 되면 로그인 된 것으로 처리하지만, pod B 로 요청이 가게 되면 로그인하지 않은 상태일 것이다.
  • 위와 같은 경우 로그인 상태를 캐싱하고 있는 서버를 별도로 두는 것이 좋다. Pod B 에서 캐싱 서버로 요청을 보내 사용자의 로그인 여부를 판단할 수 있게 될 것이다.

Current Ingress resource example

Why does Ingress controller select pods directly, and not pass the request to the service?

  • ?

왜 pod 의 YAML 에서 설정하는 것이 아니고 rc 에서 하는 것일까? 현실에서는 어차피 복제될 pod 라 괜찮겠지만...

  • Pod 를 실제로 복제할 때 readiness probe 에 대한 정보도 필요하므로 rc 의 template 에 넣어야 맞을 것이다.

When would we actually use publishNotReadyAddresses=True ?

FQDN 사용 시 cluster.local 이 아닌 곳으로도 접근이 가능한 경우가 있을까?

FQDN

DNS Records

대표적인 records

  • A record: 도메인에 IP 를 할당한다.
  • CNAME record: Canonical NAME record 으로, 특정 도메인을 다른 도메인 이름으로 매핑한다.

Load Balancer 이야기

Client 가 여러 pod 에 동시에 연결하는 상황의 예시

  • 동영상 스트리밍 할 때, 자연스럽게 화질을 전환하는 경우!
  • 고화질, 중화질, 저화질 서버를 각각 pod 으로 만들어 둔 뒤, client 는 모든 pod 에 연결하고, 네트워크 상황에 따라 pod 을 바꿔가며 동영상을 스트리밍 하면 된다.

Can a single K8s pod host 2 or more K8s services?

JSONPath / XPath

댓글