본문 바로가기
Framework and Tool/Kubernetes

Kubernetes - External service, Service export

by ocwokocw 2022. 10. 24.
- 출처: Kubernetes in action

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

지금까지는 모두 클러스터 내부의 파드에 대해서만 통신을 지원하는 서비스를 설명했다. 하지만 쿠버네티스는 외부 서비스를 노출해서 외부 IP과 포트로 연결을 전달할 수 있다. 즉 클러스터 내부 클라이언트 파드에서 외부 서비스로 연결이 가능하다.

- 서비스 엔드포인트

서비스와 파드 연결 과정에는 숨은 리소스가 있다. 서비스는 파드에 직접 연결되는것이 아니라 서비스 - 엔드포인트 리소스 - 파드로 연결된다. "kubectl describe svc"명령어로 서비스 정보를 확인해보자.
 
 
 
항목을 보다보면 엔드포인트(Endpoints)란 속성을 확인할 수 있을것이다.
 
엔드포인트 리소스는 서비스로 노출되는 파드의 IP와 포트 목록이다. 엔드포인트 리소스도 엄연한 하나의 리소스이므로 "kubectl get" 명령어로 확인이 가능하다.
 
 
 
서비스 스펙에는 파드 셀렉터가 정의되어 있다. 앞서 서비스는 파드에 직접 연결되는것이 아니라 엔드포인트 리소스를 거친다고 했다. 따라서 서비스 스펙의 파드 셀렉터는 서비스가 파드를 직접 선택하는데 사용되는것이 아니라 엔드포인트 리소스의 목록을 작성하는데 사용됨을 알 수 있다. 클라이언트가 서비스에 연결하면 서비스 프록시는 이 엔드포인트 리소스에 나타나는 목록중 하나의 파드에 요청을 전달한다.
 

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

여태까지 서비스를 생성하면 서비스 뿐만 아니라 엔드포인트를 자동으로 생성된것이다. 서비스와 엔드포인트 리소스를 분리해서 수동으로 구성할수도 있다. 어떻게 가능할까?
 
바로 앞서 "파드 셀렉터를 이용하여 엔드포인트 리소스 목록을 작성한다."고 했으므로 파드 셀렉터 없이 서비스를 생성하면 된다. 셀렉터가 없는 서비스 yaml 을 이용하여 엔드포인트 없는 서비스를 생성해보자.
 
apiVersion: v1
kind: Service
metadata:
  name: external-service
spec:
  ports:
  - port: 80
 
 
 
Endpoints가 <none>이므로 의도한대로 생성되었다. 이제 해당 서비스에 대한 엔드포인트 리소스를 수동으로 구성하자.
 
apiVersion: v1
kind: Endpoints
metadata:
  name: external-service
subsets:
  - addresses:
    - ip: 11.11.11.11
    - ip: 22.22.22.22
    ports:
      - port: 80
 
여기서 한 가지 주의할점이 있다. 위의 yaml을 자세히 보자. 서비스나 엔드포인트 yaml 에서 파드 셀렉터 같이 상호간의 연관성이 언급된 부분이 없는데 어떻게 생성된 엔드포인트 서비스가 우리가 앞에서 생성한 external-service의 엔드포인트 리소스라는것을 의미할 수 있는가? 다른 서비스의 엔드포인트 리소스라고 봐도 되는것이 아닌가? 라는 의문이 들 수 있다.
 
힌트는 엔드포인트 리소스를 자동으로 생성한 서비스에서 찾을 수 있다. 앞에서 우리가 엔드포인트 리소스를 조회하기 위해 "kubectl get endpoints"로 조회할 때 자연스럽게 서비스 이름과 동일한 이름으로 조회했다. 수동으로 생성한 엔드포인트 리소스 이름도 해당 서비스의 이름을 사용하면 된다.
 
 
 
Endpoints 항목을 보면 우리가 수동으로 작성한 엔드포인트 리소스의 IP와 포트가 적시된것을 확인할 수 있다.
 
서비스와 엔드포인트 리소스가 서버에 모두 배포되면 일반 서비스처럼 사용이 가능하다. "서비스가 생성된 후 만들어진 컨테이너"는 서비스 환경변수가 포함된다. 파드 -> 서비스(with 엔드포인트) -> 외부 서비스(외부 서버)로 요청을 할 수 있게 되었다.
 
만약 추후 외부 서비스를 쿠버네티스 클러스터 내부로 마이그레이션 하기로 결정했다면 서비스에 셀렉터를 추가해서 엔드포인트를 자동으로 관리하면 된다.
 

- 외부 서비스를 위한 별칭 생성

서비스의 엔드포인트 리소스를 수동으로 구성하는 방식대신 FQDN으로 대체할 수 있다.
 
서비스 유형을 ExternalName으로 하면 외부 서비스의 별칭(alias)으로 사용되는 서비스를 생성할 수 있다. 만약 api.somecompany.com 공개 API 가 있다고 가정해보자.
 
apiVersion: v1
kind: Service
metadata:
  name: external-service
spec:
  type: ExternalName
  externalName: api.somecompany.com
  ports:
  - port: 80
 
위의 서비스가 생성되면 서비스의 FQDN이 아닌 "external-service.deafult.svc.cluster.local" 도메인 이름으로 외부서비스에 연결할 수 있다.
 
그냥 곧바로 연결하면 되는게 아닌가? 왜 굳이 중간에 단계를 둬서 은닉화하는가? 라는 의문이 들 수 있는데 장점이 있다. 나중에 externalName이나 유형을 다시 ClusterIP로 변경하면 다른 서비스를 가리킬 수 있다.
 
ExternalName 서비스는 DNS 레벨에서만 구현된다. 서비스를 연결하는 Client는 서비스 프록시를 완전히 무시하고 외부 서비스에 직접 연결한다. 따라서 ExternalName 유형의 서비스는 ClusterIP를 얻을 수 없다.

- 외부 Client에 서비스 노출

지금부터는 클러스터 내부의 파드가 외부의 서비스를 이용하는 방법이 아닌 외부 Client가 클러스터 내부 파드로 접근하는법을 소개한다. 파드 -> 서비스 -> 외부 서비스의 반대 순서대로 외부 클라이언트 -> 서비스 -> 파드로 요청을 전달한다.
 
외부에서 내부 서비스를 접근할 때는 아래와 같은 방법 중 선택한다.
 
  • 노드포트로 서비스 유형 설정: 클러스터 노드는 노드 자체에서 포트를 열고 해당 포트로 수신된 트래픽을 서비스에 전달(외부 Client -> 노드 포트 -> 서비스)
  • 로드밸런서 서비스 유형 설정(노드 포트 확장): 쿠버네티스가 실행중인 클라우드 인프라에서 프로비저닝된 전용 로드밸런서를 사용한다. 로드밸런서가 모든 노드의 노드포트로 트래픽을 전달한다. (외부 Client -> 로드밸런서 IP -> 노드포트 -> 서비스)
  • 단일 IP 주소로 여러 서비스를 노출하는 인그레스 리소스 생성: HTTP (7계층)에서 동작하므로 TCP/UDP (4계층) 보다 풍부한 기능을 제공한다.
 

- 노드 포트 서비스 사용

노드포트 서비스를 생성하려면 쿠버네티스에서 모든 노드에 특정 포트를 할당하고 (모든 노드에서 동일한 포트 번호를 사용한다.) 서비스를 구성하는 파드로 연결을 전달한다.
 
일반 서비스인 ClusterIP와 유사하지만 서비스의 내부 클러스터 IP 뿐만 아니라 모든 노드의 IP와 할당된 포트로 서비스 접근이 가능하다. 노드 포트 서비스를 한번 생성해보자.
 
apiVersion: v1
kind: Service
metadata:
  name: kubia-nodeport
spec:
  type: NodePort
  ports:
  - port: 80
    targetPort: 8080
    nodePort: 30123
  selector:
    app: kubi
 
nodePort는 생략하면 쿠버네티스가 임의의 포트를 할당한다.
 
 
 
PORT열을 확인해보면 yaml에 적시한 포트를 사용한것을 알 수 있다. 이제 클러스터 IP와 노드 IP로 서비스에 접근이 가능하다. (10.110.242.136:80, 1번째노드IP:30123, 2번째노드IP:30123, ...)
 
여기서 헷갈릴 수 있는 부분이 있는데 외부 Client -> 노드1 로 요청이 전달됐을 때 반드시 노드1 내의 파드로 요청이 전달되지 않는다는 사실이다. 요청이 외부 Client -> 노드1 -> 서비스로 전달되고, 서비스는 엔드포인트 리소스의 목록을 참고하여 전달하기 때문에 다른 노드의 파드로 요청이 전달될수도 있다. (엔드포인트 리소스 목록에는 모든 노드의 파드들 IP와 포트 목록이 존재할것이기 때문이다.)
 

- 외부 로드밸런서로 서비스 노출

클라우드 공급자(AWS, GCP, Azure)에서 실행되는 쿠버네티스 클러스터들은 일반적으로 클라우드 인프라에서 로드 밸런서를 자동으로 프로비저닝하는 기능을 제공한다. 유형을 노드포트대신 로드밸런서로 지정하면 되며, 공개적으로 접근 가능한 고유 IP를 가지며 모든 연결을 서비스로 전달한다.
 
로드밸런서 서비스는 노드 포트 서비스의 확장형이다. 로드 밸런서를 지원하지 않는 환경이라면 로드 밸런서가 프로비저닝되지 않지만 서비스는 노드 포트 서비스처럼 작동한다. 로드 밸런서 서비스를 생성해보자.
 
apiVersion: v1
kind: Service
metadata:
  name: kubia-lb
spec:
  type: LoadBalancer
  ports:
  - port: 80
    targetPort: 8080
  selector:
    app: kubia
 
 
 
특정 노드포트를 지정할 수 있지만 공란으로 남겨두어 쿠버네티스가 포트를 선택하게 한다.
 
서비스를 생성하면 클라우드 인프라가 로드 밸런서를 생성하고 IP 주소를 서비스 오브젝트에 쓴다. 우리는 minikube 라서 pending으로 나오지만 로드밸런서를 지원한다면 EXTERNAL-IP에 로드 밸런서 IP가 나오게 된다.
 
파드까지 요청 과정은 외부 클라이언트 -> 로드 밸런서 -> 노드 포트 -> 서비스 -> 파드 순으로 전달된다.

- 외부 연결의 특성

외부 클라이언트가 노드 포트 서비스에 접속하는 경우(로드 밸런서 타입도 포함) 요청이 아래 2가지 경로로 전달될 수 있다.
 
  • 동일 노드내의 파드로 전달
  • 다른 노드의 파드로 전달
 
다른 노드로 전달되는 경우 추가적인 네트워크 홉이 발생한다. 만약 이 추가적인 홉이 발생하는게 싫다면 연결을 수신한 노드에서 실행중인 파드로만 요청을 전달하는 옵션을 설정할 수 있다. spec.externalTrafficPolicy를 local로 설정하면 된다.
 
만약 로컬 파드가 존재하지 않으면 연결은 중단되므로 로드 밸런서는 해당 파드가 하나 이상 있는 노드에만 연결을 전달하도록 해야 한다.
 
이 어노테이션은 또 하나의 단점을 갖고 있는데 부하분산이 제대로 되지 않는다는것이다. 만약 노드 2개와 파드 3개가 있고 파드가 각 노드에 1, 2개로 나뉘어 있다고 가정해보자. 각 노드로 부하를 제대로 50:50으로 분산한다고 해도 1 노드에는 파드가 2개 이므로 결국 파드 입장에서 50:25:25로 부하가 분산된다.
 
클라이언트 IP가 보존되는 문제도 생각해볼 수 있다. 웹에서 접근 로그를 기록하는 경우 클라이언트 IP를 기록하는 경우가 많다. 일반적으로 클러스터 내 클라이언트가 서비스로 연결시 서비스의 파드는 클라이언트 IP 주소를 획득할 수 있다.
 
하지만 노드포트로 연결 수신시 패킷에서 소스 네트워크 주소 변환(SNAT)가 수행되어 소스 IP가 변경된다.
 
 

'Framework and Tool > Kubernetes' 카테고리의 다른 글

Kubernetes - readness probe, headless service  (0) 2022.10.30
Kubernetes - ingress  (0) 2022.10.30
Kubernetes - Service intro  (0) 2022.10.21
Kubernetes - job and cron job  (0) 2022.10.21
Kubernetes - ReplicaSet, DaemonSet  (0) 2022.10.14

댓글