Service

유웅조·2020년 9월 15일
0

kubernetes

목록 보기
3/4

Service

마이크로서비스에서의 파드는 사실상 클러스터 외부의 HTTP 통신에 응답할 수 있어야 한다.

또한 파드가 다른 파드에게 제공하는 서비스를 사용하려면 다른 파드를 찾는 방법이 필요하다.

클러스터 노드에 파드가 스케줄링되면 파드에 IP가 부여되기는 하지만 이는 일시적이다.

파드는 언제든 다른 파드로 교체될 수도 있다.

또 클라이언트는 미래에 생길 IP 주소를 알 수 없기에 파드의 개별 IP를 사용할 수는 없다.

이런 문제를 해결하기 위해 서비스라는 리소스 유형을 제공한다.

소개

서비스는 파드 그룹에 지속적인 단일 접점을 만들려고 할 때 생성되는 리소스로 바뀌지 않는 IP 주소와 포트가 있다.

클라이언트는 해당 IP와 포트로 접속한 다음, 해당 서비스를 지원하는 파드 중 하나로 연결된다.

예시

프론트엔드 웹 서버와 백엔드 데이터베이스 서버가 있다고 가정하고, 프론트엔드에는 여러 개의 파드, 데이터베이스 파드는 하나만 있다고 가정한다.

이때, 웹 서버의 갯수에 상관없이 클라이언트는 프론트엔드 파드에 연결할 수 있어야 한다.

그리고 프론트엔드 파드는 클러스터 안에서 변경되는 IP 주소에 구애받지 않고, 백엔드 데이터베이스에 연결되어야 한다.

이와 같은 조건은 다음으로 충족시킬 수 있다.

  1. 프론트엔드 파드에 관한 서비스를 만들고 클러스터 외부에서 엑세스할 수 있도록 고정 IP 주소를 노출한다.

  2. 백엔드 파드에 관한 서비스를 생성해 고정적인 IP 주소를 생성한다.

  3. 프론트엔드 파드에서 환경변수 또는 DNS 이름으로 백엔드 서비스를 찾는다.

생성

서비스에서도 레플리케이션컨트롤러와 기타 파드 컨트롤러에서처럼 레이블 셀렉터를 사용해서 동일한 셋에 속하는 파드를 지정할 수 있다.

가장 간단하게 서비스를 생성하는 방법은 kubectl expose 명령어를 사용하면 된다.

기본적인 yaml 파일은 다음과 같다.

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

kubectl get svc 하게 되면 생성한 service 를 확인할 수 있다.

kube exec {파드이름} -- curl -s http://{서비스IP주소} 를 활용하여 기존 파드에서 curl 명령어를 실행하여 확인해보면 정상적으로 작동함을 알 수 있다.

반복적으로 위의 요청을 보내게 되면 서비스 프록시가 임의의 파드로 연결한다는 것을 알 수 있다.

만약 특정 클라이언트의 모든 요청을 같은 파드로 연결하기를 원한다면 서비스의 session Affinity 속성을 None => ClientIP로 설정한다.

apiVersion: v1
kind: Service
spec:
  sessionAffinity: ClientIP
...

만약 같은 IP 에서 다른 PORT 를 노출하기를 원한다면 다음과 같은 멀티 포트 서비스를 사용하면 된다.

apiVersion: v1
kind: Service
metadata:
  name: kubia
spec:
  ports:
  - name: http
    port: 80
    targetPort: 8080
  - name: https
    port: 443
    targetPort: 8443
selector:
  app: kubia

이름이 지정된 포트 사용

각 파드의 포트에 이름을 지정하고 서비스 스펙에서 이름으로 참조하는 것도 가능하다.

apiVersion: v1
kind: Pod
spec:
  containers:
  - name: kubia
    ports:
    - name: http
      containerPort: 8080
    - name: https
      containerPort: 8443
apiVersion: v1
kind: Service
spec:
  ports:
  - name: http
    port: 80
    targetPort: http
  - name: https
    port: 443
    targetPort: https

위와 같은 방식을 사용하면 포트의 세부 정보를 몰라도 이름으로 참조하여 유용하게 사용하는 것이 가능하다.

클라이언트에서 서비스의 IP를 미리 알수는 없다. 따라서 쿠버네티스에서는 서비스의 IP와 포트를 검색할 수 있는 방법을 제공한다.

환경변수를 통한 서비스 검색

kube exec {파드이름} env 을 사용하면 해당 파드 안에 {서비스이름}_SERVICE으로 환경변수가 들어가 있는 것을 알 수 있다.

위의 예시에서 프론트엔드 파드가 백엔드 파드의 서비스에 접근하고 싶다면 파드의 환경변수에서 서비스의 주소와 포트를 찾을 수 있을 것이다.

DNS 를 통한 서비스 검색

kube-system 네임스페이스에는 동일한 이름의 해당 서비스가 있다. 이름처럼 DNS 서버를 실행하며 클러스터에서 실행 중인 다른 모든 파드는 자동으로 이를 사용하도록 구성된다.

파드에서 실행 중인 프로세스에서 수행된 모든 DNS 쿼리는 시스템에서 실행 중인 서비스를 알고 있는 쿠버네티스의 자체 DNS 서버로 처리된다.

각 서비스는 내부 DNS 서버에서 DNS 항목을 가져오고 서비스 이름을 알고 있는 클라이언트 파드는 환경변수 대신 FQDN(정규화된 도메인 이름) 으로 엑세스할 수 있다.

FQDN

다시 위의 예재로 돌아가서 프론트엔드 파드는 다음 FQDN 을 사용하여 백엔드 서비스에 연결할 수 있다.

{서비스이름}.default.svc.cluster.local

default 는 서비스가 정의된 네임스페이스를 나타낸다.

svc.cluster.local 은 모든 클러스터의 로컬 서비스 이름에 사용되는 클러스터의 도메인 접미사다.

kube exec -it {파드이름} bash 이 명령어를 통해 해당 파드의 컨테이너 내에 접속하여 curl을 날려본다.

서비스에 PING-PONG?

서비스 IP의 작동 여부를 확인하고 싶다면...

root@{파드이름}:/# ping kubia
PING kubia.default.svc.cluster.local (10.103.198.138): 56 data bytes

서비스로 curl은 동작하지만 핑은 응답이 없다. 왜냐하면 서비스의 클러스터 IP가 가상 IP 이기 때문에 서비스 포트와 결합된 경우에만 의미가 있기 때문이다.

클러스터 외부에 있는 서비스 연결

서비스가 외부 IP와 포트로 연결을 전달하려는 경우 서비스 로드밸런싱과 서비스 검색 모두 활용할 수 있다.

클러스터에서 실행 중인 클라이언트 파드는 내부 서비스에 연결하는 것처럼 외부 서비스에 연결할 수 있다.

서비스는 파드에 직접 연결되지 않는다. 대신 엔드포인트 리소스가 그 사이에 있다.

kube describe svc kubia                                       
Name:              kubia
Namespace:         default
Labels:            <none>
Annotations:       <none>
Selector:          app=kubia
Type:              ClusterIP
IP:                10.103.198.138
Port:              <unset>  80/TCP
TargetPort:        8080/TCP
Endpoints:         172.18.0.6:8080,172.18.0.7:8080,172.18.0.8:8080
Session Affinity:  None
Events:            <none>

위에서 엔드포인트 리소스를 확인할 수 있는데, 파드의 IP 주소, 포트 목록을 볼 수 있다.

파드 셀렉터는 서비스 스펙에 정의돼 있지만 들어오는 연결을 전달할 때 직접 사용하지는 않는다.

대신 셀렉터는 IP와 포트 목록을 작성하는 데 사용되며 엔드포인트 리소스에 저장된다.

클라이언트가 서비스에 연결하면 서비스 프록시는 이들 중 하나의 IP와 포트 쌍을 선택하고 들어온 연결을 대상 파드의 수신 대기 서버로 전달한다.

서비스 엔드포인트 수동 구성

서비스의 엔드포인트를 서비스와 분리하면 엔드포인트를 수동으로 구성하고 업데이트할 수 있다.

파드 셀렉터 없이 서비스를 만들면 쿠버네티스는 엔드포인트 리소스를 만들지 못한다.

apiVersion: v1
kind: Service
metadata:
  name: external-service
spec:
  ports:
  - port: 80

위처럼 서비스에 대한 파드 셀렉터를 정의하지 않고 서비스 자체에 대한 yaml 을 만든다.

엔드포인트는 별도의 리소스이고 서비스 속성이 아니다.

다음은 엔드포인트 서비스이다.

apiVersion: v1
kind: Endpoints
metadata:
  name: external-service
subsets:
  - addresses:
    - ip: 11.11.11.11
    - ip: 22.22.22.22
    ports:
    - port: 80

엔드포인트 오브젝트는 서비스와 이름이 같아야 하고 서비스를 제공하는 대상 IP 주소와 포트 목록을 가져야 한다.

서비스가 만들어진 후 만들어진 컨테이너에는 서비스의 환경변수가 포함되며 IP:PORT 쌍에 대한 모든 연결은 서비스 엔드포인트 간에 로드밸런싱한다.

외부 서비스의 별칭으로 사용되는 서비스를 만들 수 있다.

apiVersion: v1
kind: Service
metadata:
  name: external-service
spec:
  type: ExternalName
  externalName: someapi.somecompany.com
  ports:
  - port: 80

위의 externalName key 에 해당하는 것이 실제 서비스의 정규화된 도메인 이름이다.

서비스를 생성하고 나면 파드는 external-service 혹은 external-service.default.svc.cluster.local이라는 이름으로 외부 서비스에 접근할 수 있게 된다.

외부 클라이언트에 서비스 노출

여태까지는 클러스터 내부에서 파드가 서비스를 활용하는 방법에 관해 알아보았다. 이제는 특정 서비스를 외부에 노출해 클라이언트가 접근할 수 있도록 하는 방법에 대해 알아본다.

외부에서 서비스에 액세스할 수 있는 방법은 몇 가지가 있다.

  1. NodePort로 서비스 유형 설정: 각 클러스터 노드는 노드 자체에서 포트를 열고 해당 포트로 수신된 트래픽을 서비스로 전달한다.

  2. 서비스 유형을 NodePort의 확장인 LoadBalancer로 설정: 실행 중인 클라우드 인프라에서 프로비저닝된 전용 로드밸런서로 서비스에 액세스할 수 있다. 로드밸런서가 트래픽을 모든 노드의 노드포트로 전달한다. 클라이언트는 로드밸런서의 IP로 서비스에 접근한다.

  3. 단일 IP 주소로 여러 서비스를 노출하는 인그레스 리소스 만들기: HTTP 레벨에서 작동하기 때문에 서비스보다 많은 기능을 제공할 수 있다.

NodePort 서비스 사용

서비스를 생성하고 유형을 노드포트로 설정한다. 노드포트 서비스를 만들면 쿠버네티스는 모든 노드에 특정 포트를 할당하고 서비스를 구성하는 파드로 들어오는 연결을 전달한다.

모든 노드의 IP와 할당된 노드포트로 서비스에 액세스할 수 있다.

NodePort 생성 기본
apiVersion: v1
kind: Service
metadata:
  name: kubia-nodeport
spec:
  type: NodePort
  ports:
  - port: 80
    targetPort: 8080
    nodePort: 30123
  selector:
    app: kubia

minikube 의 경우 minikube service kubia-nodeport로 노드포트 서비스에 액세스하여 확인해볼 수 있다.

위와 같이 설정할 경우 문제점은 요청을 받는 노드에 장애가 생길 경우, 클라이언트가 더 이상 서비스에 액세스할 수 없다는 점이다.

따라서 노드포트 서비스 대신 로드밸런서를 생성해 로드밸런서를 자동으로 프로비저닝 하는 방법이 있다.

LoadBalancer 생성

쿠버네티스 클러스터는 일반적으로 클라우드 인프라에서 로드밸런서를 자동으로 프로비저닝하는 기능을 제공한다.

로드밸런서는 접근 가능한 고정 IP를 가지며 모든 연결을 서비스로 전달한다. 따라서 로드밸런서의 IP 주소로 서비스에 접근할 수 있다.

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

위의 예시에 추가적으로 특정 노드포트를 지정할 수도 있다.

외부 연결에 관해 알아야 할 몇 가지

  • 불필요한 네트워크 홉의 이해와 예방

    외부 클라이언트가 노드포트로 서비스에 접속할 경우 (로드밸런서를 통과하는 경우도 포함) 임의로 선택된 파드가 연결을 수신한 동일한 노드에서 실행 중일 수도, 그렇지 않을 수도 있다.

    따라서 파드에 도달하려면 추가적인 Network Hop 의 가능성이 있다.

    외부 연결을 수신한 노드에서 실행 중인 파드로만 외부 트래픽을 전달하도록 서비스를 구성해 추가 홉을 방지할 수 있다.

    externalTrafficPolicy: Localspec 필드에 추가해주면 된다.

    과정은 서비스 프록시는 로컬에 실행 중인 파드를 선택 > 로컬 파드가 없으면 연결 중단 으로 이어진다.

    그렇기 때문에 로컬에 파드가 하나 이상 있는 노드에만 연결을 전달하도록 해야 한다.

    추가적으로 이 어노테이션을 사용할 경우 모든 파드에 더 이상 균등하게 연결이 전달되지 않는다.

      두 개의 노드가 있다. 하나의 노드에는 파드A. 다른 하나의 노드에는 파드B, 파드C.
      로드밸런서가 각각의 노드에 50% 연결을 전달한다. 
      첫번째 노드에는 파드A가 50%를 받는다. 
      두번째 노드에는 파드B, 파드C가 각각 25%씩 받는다.
      더 이상 밸런싱은 일어나지 않는다. 
  • 클라이언트 IP가 보존되지 않음 인식

    일반적으로 클러스터 내의 클라이언트가 서비스로 연결할 때 서비스의 파드는 클라이언트의 IP 주소를 얻을 수 있다.

    그러나 노드포트로 연결을 수신하면 패킷에서 소스 네트워크 주소 변환이 수행되므로 패킷의 소스 IP가 변경된다.

    따라서 클라이언트의 IP를 알 수 없는 문제가 발생한다.

    이전 절에서 이야기한 LocalExternalTrafficPolicy는 연결을 수신하는 노드와 대상 파드를 호스팅하는 노드 사이에 추가 홉이 없기 때문에 소스 네트워크 주소 변환이 수행되지 않는다.

Ingress 로 서비스 외부 노출

인그레스는 로드밸런서와 다르게 한 IP 주소로 수십 개의 서비스에 접근이 가능하도록 지원해준다.

바꿔 말하면 여러 개의 서비스를 한 개의 인그레스로 노출할 수 있다는 것을 의미한다.

인그레서는 7 계층 중에서 어플리케이션 계층에서 작동하기 때문에 서비스가 할 수 없는 쿠키 기반 세션 어피니티 등과 같은 기능을 제공한다.

인그레스 오브젝트를 살펴보기 이전에 먼저 인그레스 컨트롤러를 클러스터에 실행해야 한다.

쿠버네티스 환경마다 다른 컨트롤러 구현을 사용할 수 있지만 일부는 기본 컨트롤러를 전혀 제공하지 않는다.

minikube addons enable ingress로 ingress-controller 를 생성한다.

현재 macOS 및 Window 에서는 minikube addons enable ingress가 불가능하다. 
--vm=true 로 minikube를 재시작한 뒤에는 가능하다. 

Ingress 서비스 생성

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: kubia
spec:
  rules:
  - host: kubia.example.com
    http:
      paths:
      - path: /
        backend:
          serviceName: kubia-nodeport
          servicePort: 80

인그레스는 kubia.example.com 도메인 이름을 서비스에 매핑한다.

모든 요청은 kubia-nodeport 서비스의 40번 포트로 전달된다.

Ingress 동작 방식

클라이언트는 먼저 kubia.example.com 의 DNS 조회를 수행한다.

조회 결과 Ingress 컨트롤러의 IP를 반환 받는다.

그런 다음 HTTP 요청을 인그레스 컨트롤러로 전송하고 host 헤더에서 kubia.example.com을 지정한다.

컨트롤러는 해당 헤더에서 클라이언트가 액세스하려는 서비스를 결정하고 서비스와 관련된 엔드포인트 오브젝트로 파드 IP를 조회한다.

클라이언트 요청을 맞는 파드에 전달한다.

여러 개의 서비스 노출하기

인그레스의 스펙을 조정하면 여러 호스트, 경로, 서비스를 지정할 수 있다.

  1. 먼저 경로의 경우...
spec:
  rules:
  - host: kubia.example.com
    http:
      paths:
      - path: /kubia
        backend:
          serviceName: kubia
          servicePort: 80
      - path: /bar
        backend:
          serviceName: bar
          servicePort: 80

위의 경우 /kubia, /bar 두 경로에 따라 다른 서비스로 전송된다.

  1. 호스트의 경우...
spec:
  rules:
  - host: foo.example.com
    http:
      paths:
      - path: /
        backend:
          serviceName: foo
          servicePort: 80
  - host: bar.example.com
    http:
      paths:
      - path: /
        backend:
          serviceName: bar
          servicePort: 80

컨트롤러가 수신한 요청은 호스트 헤더에 따라 foo, bar로 매핑된다. 단, 이 경우엔 DNS 에 foo.example.com, bar.exmaple.com이 인그레스 컨트롤러의 IP로 지정되어야 한다.

TLS 트래픽을 처리하도록 인그레스 구성

HTTPS 에서의 인그레스를 간단하게 살펴본다.

클라이언트가 인그레스 컨트롤러에 대한 TLS 연결을 하면 컨트롤러는 TLS 연결을 종료한다.

클라이언트와 컨트롤러 간의 통신은 암호화되지만 컨트롤러와 백엔드 파드 간의 통신은 암호화되지 않는다.

결국 컨트롤러가 TLS 와 관련된 모든 것을 처리하도록 할 수 있다는 것이다.

그렇게 하려면 먼저 인증서와 개인 키를 인그레스에 첨부해야 한다. 이 두 개는 secret이라는 쿠버네티스 리소스에 저장하며 인그레스 매니페스트에서 참조한다.

openssl genrsa -out tls.key 2048
openssl req -new -x509 -key tls.key -out tls.cert -days 360 -subj /CN=kubia.exmple.com
kube create secret tls tls-secret --cert=tls.cert --key=tls.key

위의 명령어를 통해 개인 키와 인증서를 tls-secret 이라는 시크릿 에 저장한다.

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: kubia
spec:
  tls:
  - hosts:
    - kubia.example.com
    secretName: tls-secret
  rules:
  - host: kubia.example.com
    http:
     paths:
       - path: /
         backend:
           serviceName: kubia-nodeport
           servicePort: 80

위는 기본적인 tls 인그래스 생성 파일이다.

Check Readiness

새로운 파드가 기존에 서비스가 택한 셀렉터와 레이블이 같다면, 그 파드 역시 서비스의 일부가 되어 요청이 파드로 전달될 것이다.

하지만 만약 아직 그 파드가 요청을 처리할 준비가 안 되어 있다면 어떻게 될까.

따라서 완전히 준비될 때까지 파드에게 요청을 전달하지 않을 필요가 있다.

여기서 사용되는 것이 READINESS Probe 이다.

라이브니스프로브는 불안전한 컨테이너를 찾아 자동으로 재시작하는 역할을 담당했다면, 레디니스프로브는 비슷하지만 조금 다른, 준비가 되어 있는가에 대한 조회를 수행한다.

라이브니스프로브와 유사하게 3 가지 유형의 레디니스 프로브가 있다.

  • Exec Probe: 컨테이너의 상태를 프로세스의 종료 상태 코드로 결정
  • HTTP GET Probe: HTTP GET 요청을 컨테이너로 보내 응답 코드로 결정
  • TCP Socket Probe: TCP 연결을 시도, 결과에 따라 결정

레디니스프로브는 언뜻 라이브니스프로브와 비슷하게 보이지만, 가장 큰 차이는 결과 후의 반응에 대한 것이다.

만약 레디니스프로브가 정상 코드를 받지 못했다면, 엔드포인트 오브젝트에서 파드를 제거하여 요청을 수신하지 못하도록 막는다.

ReadinessProbe 생성

...
readinessProbe:
  exec:
    command:
    - ls
    - /var/ready

실제 환경에서는 해당 파드가 정말 클라이언트의 요청을 받을 준비가 되어 있는지에 대해 테스트 해보아야 한다.

Headless Service

지금까지는 클라이언트의 연결을 허용하기 위해 안정적인 IP 주소를 갖는 방법을 알아 보았다.

그러나 클라이언트가 모든 파드에 연결해야 하는 경우 어떻게 할 수 있을까. 혹은 파드가 다른 파드에 각각 연결해야 하는 경우 어떻게 해야 할까.

클라이언트가 모든 파드에 연결하려면 각 파드의 IP를 알아야 한다. => 클라이언트가 쿠버네티스 API 서버를 호출해 파드와 IP 주소 목록을 가져오도록 할 수 있다. 하지만 좋은 방법은 아니다.

다행히 쿠버네티스는 DNS 조회로 파드 IP를 찾을 수 있도록 한다.

쿠버네티스 서비스에 클러스터 IP가 필요하지 않다면, DNS 서버는 하나의 서비스 IP 대신 파드 IP 들을 반환한다.

따라서 클라이언트는 DNS A 레코드 조회를 수행하고 서비스에 포함도니 모든 파드의 IP를 얻을 수 있다.

서비스 스펙의 clusterIP 필드를 None으로 설정하면 쿠버네티스는 클라이언트가 서비스의 파드에 연결할 수 있는 컬러스터 IP를 할당하지 않기 때문에 서비스가 헤드리스 상태가 된다.

다음은 기본적인 헤드리스 서비스 생성 파일이다.

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

kube run dnsutils --image=tutum/dnsutils --generator=run-pod/v1 --command -- sleep infinity

위 명령어로 간단하게 DNS 조회를 수행할 수 있는 파드를 생성한 뒤 확인해 본다.

헤드리스 서비스는 클라이언트 관점에서는 일반 서비스와 크게 다르지 않다. 마찬가지로 서비스의 DNS 이름에 연결해 파드에 연결할 수 있다.

하지만 헤드리스 서비스에서는 DNS 서버가 파드의 IP를 반환하기 때문에 클라이언트는 서비스 프록시 대신 파드에 직접 연결한다.

서비스 문제 해결 TIP

  1. 외부가 아닌 클러스터 내에서 서비스의 클러스터 IP 에 연결되는지 확인한다.
  2. 서비스에 액세스할 수 있는지 확인하려고 서비스 IP 로 핑을 할 필요 없다(가상 IP 이기 때문에 PING 되지 않는다)
  3. 레디니스 프로브를 정의했다면 성공했는지 확인 > 실패 시 파드가 서비스에 포함 X
  4. 파드가 서비스의 일부인지 확인하려면 kubectl get endpoints로 엔드포인트 오브젝트 화인
  5. FQDN 이나 그 일부로 서비스에 액세스하려고 하는데 작동하지 않는 경우, FQDN 대신 클러스터 IP를 사용해 본다.
  6. 대상 포트가 아닌 서비스로 노출된 포트에 연결돼 있는지 확인한다.
  7. 파드 IP 로 직접 연결해 파드가 올바른 포트에 연결돼 있는지 확인한다.
  8. 파드 IP 로 애플리케이션에 액세스할 수 없는 경우 애플리케이션이 로컬호스트에만 바인딩하고 있는지 확인한다.

0개의 댓글