[Kubernetes] 서비스와 인그레스, 레디니스 프로브

서비스

서비스는 쿠버네티스의 리소스 유형이다. 동일한 서비스를 제공하는 파드 그룹에 지속적인 단일 접점을 만들고자 할 때 생성하는 리소스다.

파드의 IP 주소는 변경될 수 있지만 서비스의 IP는 변경되지 않는다. 또, 서비스를 생성하면 클라이언트 파드가 환경변수 또는 DNS 이름으로 백엔드 서비스를 쉽게 찾을 수 있다.

서비스의 연결은 모든 파드로 로드밸런싱된다. 실제 어떤 파드로 연결되느냐는 로드밸런싱 뿐만 아니라 세션 어피니티에 따라서도 차이가 있다.

기본적인 세션 어피니티는 None으로 설정되는데, 이 경우 매 요청시마다 서로 다른 파드가 응답을 줄 수 있다. 반면 세션 어피니티를 ClientIP로 설정하면 동일한 클라이언트 IP에 대해서는 동일한 파드가 응답하게 할 수도 있다.

서비스 생성

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

kubectl -f create {서비스명} 명령으로 서비스를 생성할 수 있다.

서비스가 파드를 찾는 법

서비스가 파드를 찾아 연결하게 하는 방법은 여러 가지가 있다.

  1. 포트 번호 명시: 서비스의 스펙에 연결해야할 파드의 포트 번호를 명시한다.
  2. 이름이 지정된 포트 사용: 특정 포트 번호를 명시하기 보다는 파드의 포트번호에 이름을 붙이고 이름으로 찾는다. 포트번호가 변경되도 서비스는 파드를 연결할 수 있어 더 낫다.

클라이언트 파드가 서비스를 찾는 법

서비스는 고정된 IP를 갖지만, 클라이언트 파드를 생성할 때 매번 서비스의 IP를 전달하는 것은 복잡하다. 클라이언트 파드에서 서비스를 찾는 방법을 알아보자.

  1. 환경변수에서 검색: 서비스를 생성하면 클라이언트 파드는 프로세스 환경변수를 검사해 서비스의 IP주소와 포트를 얻을 수 있다.
  2. DNS를 통한 검색: 파드에서 실행 중인 프로세스는 DNS 쿼리를 통해 서비스를 찾을 수 있다. FQDN이라는 정규화된 도메인을 통해서 가능한데. backend-database.default.svc.cluster.local이라는 주소가 있다고 한다면, backend-database는 서비스의 이름이고 default는 기본 네임스페이스를 나타내며 나머지는 모든 클러스터의 로컬 서비스 이름에 사용되는 클러스터 도메인 접미사다.

한편, 클라이언트가 서비스를 찾으려면 포트 번호를 알아야 한다. 표준 포트를 사용한다면 괜찮지만 그렇지 않은 경우 환경 변수에서 포트 번호를 얻을 수 있어야 한다.

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

특정 서비스를 외부에 노출해 외부에 위치한 클라이언트가 접근할 수 있게 하는 방법은 아래와 같다.

  1. 서비스 유형을 노드포트로 설정: 각 클러스터 노드는 자체 포트를 열고 그 포트로 수신된 트래픽을 서비스로 전달한다. 서비스는 내부 클러스터 IP와 포트로 액세스할 수 있고 모든 노드의 전용 포트로도 액세스 할 수 있다.
  2. 서비스 유형을 로드밸런서로 설정: 클라우드 서비스를 이용 중이고, 그 클라우드가 로드밸런서를 지원하는 경우, 노드포트 유형의 수퍼셋인 로드밸런서로 서비스에 액세스할 수 있다. 로드밸런서는 트래픽을 모든 노드의 노드포트로 전달한다. 클라이언트는 로드밸런스의 IP로 액세스할 수 있다.
  3. 인그레스 리소스 설정: 단일 IP 주소로 여러 서비스를 노출하는 인그레스 리소스를 이용해 HTTP 레벨에서 클라이언트가 연결할 수 있게 할 수 있다.

노드포트 서비스 사용

서비스를 생성하고 유형을 노드포트로 설정하면 된다. 쿠버네티스는 모든 노드에 특정 포트를 할당(모든 노드는 동일한 포트번호 사용)한다.

인터넷에서 어떤 노드든 특정 포트로 파드에 액세스할 수 있다. 다만, 특정 노드에만 트래픽이 몰려 장애가 나면 클라이언트는 더 이상 서비스에 액세스할 수 없으므로 로드밸런서를 배치해야 한다.

로드밸런서로 서비스 노출

클라우드 프로바이더가 로드밸런서를 제공하는 경우 (구글, AWS 등) 이 로드밸런서를 사용할 수 있다. 노드포트 대신 서비스 유형을 로드밸런서로 설정하면 된다. 이를 통해 로드밸런서의 IP 주소로 서비스에 액세스 할 수 있다.

참고로, 로드밸런서 서비스는 노드포트 서비스의 확장(수퍼셋)이다.

참고로 노드포트 서비스나 로드밸런서를 이용하는 경우 파드에 도달하기 위해서는 불필요한 네트워크 홉이 필요할 수 있다. 임의로 선택된 파드가 트래픽이 도달한 노드와 동일한 노드에서 실행되리란 보장이 없기 때문이다.

이를 막기 위해서는 externalTrafficPolicy를 Local로 설정하면 되지만, 연결이 모든 파드에 균등하게 분산되지 않게 된다는 단점이 존재한다.

인그레스 리소스로 노출

인그레스는 한 IP 주소로 수십 개의 서비스에 접근이 가능하도록 지원해준다. 인그레스는 요청한 호스트와 경로를 바탕으로 요청을 전달할 서비스를 결정한다.

인그레스는 애플리케이션 계층(HTTP)에서 작동하므로 쿠키 기반 세션 어피니티와 같은 기능도 제공할 수 있다.

인그레스

인그레스 리소스를 이용하려면 클러스터에 인그레스 컨트롤러를 실행해야 한다. 인그레스 컨트롤러를 생성하면 인그레스 리소스를 만들 수 있다.

인그레스 동작 방식

  1. 클라이언트는 DNS 조회를 통해 인그레스 컨트롤러의 IP를 조회한다.
  2. 클라이언트는 인그레스 컨트롤러로 HTTP 요청을 전송하며, host 헤더에 도메인을 지정한다.
  3. 컨트롤러는 위 헤더를 통해 클라이언트가 액세스 하려는 서비스를 특정한다.
  4. 컨트롤러는 엔드포인트 오브젝트를 이용해 파드 IP를 조회한 후 클라이언트 요청을 파드에 전달한다.

인그레스 컨트롤러는 요청을 서비스로 직접 전달하지는 않는다. 서비스를 통해 파드를 선택하고 나면, 파드에게 직접 요청을 전달한다.

인그레스를 이용해 여러 서비스 노출

하나의 인그레스로 여러 서비스를 노출할 수 있다고 했다. 인그레스 스펙을 설정할 때 규칙과 경로는 모두 배열이기 때문에 구조적으로 가능한데, 구체적으로는 다음 2가지 경우가 있다.

동일한 호스트, 다른 경로로 여러 서비스 매핑

예를들어 host: leonkong.cc이고 경로는 /posts/comments가 있다. 각 서비스는 /posts, /comments 경로에 각각 매핑될 수 있다.

파일을 살펴보면 아래와 같다.

...
  - host: leonkong.cc
    http:
      paths:
      - path: /posts
        backend:
          serviceName: post
          servicePort: 80
      - path: /comments
        backend:
          serviceName: comment
          servicePort: 80

이 경우 요청은 경로에 따라 다른 서비스로 전송된다. 클라이언트는 단일 IP 주소(인그레스 컨트롤러 IP)로 두 개의 서비스에 도달할 수 있다.

서로 다른 호스트, 서로 다른 서비스

host에 따라 서로 다른 서비스를 매핑할 수도 있다.

spec:
  rules:
  - host: api.leonkong.cc
    http:
      paths: /
      backend:
        serviceName: api
        servicePort: 80
  - host: resource.leonkong.cc
    http:
      paths: /
      backend:
        serviceName: resource
        servicePort: 80

host가 api.leonkong.cc, resource.leonkong.cc 2개가 존재한다. 컨트롤러가 수신한 요청은 요청의 호스트 헤더를 보고 api 또는 resource 서비스로 전달된다.

TLS 트래픽 처리

인그레스 설정을 통해 TLS 트래픽도 처리할 수 있다. 참고로 클라이언트와 컨트롤러 간 통신은 TLS 암호화가 되지만 컨트롤러와 파드 간 통신은 암호화하지 않는다.

인증서와 개인 키를 인그레스에 첨부하면 TLS를 지원할 수 있는데 secret이라는 쿠버네티스 리소스에 저장한다.

레디니스 프로브

레디니스 프로브는 파드가 연결 가능한 상태인지 조사하여 서비스의 엔드포인트로 포함시킬지 여부를 결정하는 것을 말한다.

라이브니스 프로브 vs 레디니스 프로브

  • 라이브니스 프로브는 컨테이너가 불완전할 경우 자동으로 다시 시작해 애플리케이션의 상태를 유지하고 자동으로 치유한다.
  • 레디니스 프로브는 파드가 클라이언트 요청을 수신할 수 있는지 확인해 엔드포인트로 노출할지 결정한다. (노출되지 않으면 클라이언트는 연결하지 않게 된다.)

즉, 라이브니스 프로브는 자연치유에 목적이 있고, 레디니스 프로브는 파드가 준비될 때까지 요청을 받지 않도록 하는 데 있다(다른 준비된 파드가 있다면 그 파드가 요청을 받을 수 있도록).

레디니스 프로브 유형

레디니스 프로브도 3가지 유형이 있다.

  • Exec 프로브: 명령을 실행해 종료 상태 코드로 컨테이너의 상태를 결정한다.
  • HTTP GET 프로브: 응답으로 성공 코드를 받으면 준비로 간주
  • TCP 소켓 프로브: TCP 소켓 연결에 성공시 준비로 간주

동작

주기적으로 프로브를 호출한다. 파드가 준비되면 서비스에 추가되며, 준비되지 않았다면 서비스에서 제거된다. 파드가 준비되지 않았다더라도 컨테이너가 종료되거나 다시 시작되지 않는다.

레디니스 프로브는 요청을 처리할 준비가 완료된 파드의 컨테이너만 요청을 수신하도록 돕는 역할을 한다.

레디니스 프로브를 사용하면 클라이언트가 정상 상태인 파드와만 통신하므로 시스템의 문제를 모르게 된다.

간단한 Exec 프로브

spec:
...
  template:
  ...
    spec:
      containers:
      - name: leon
        image: chaewonkong/simple:server
        readinessProbe:
          exec:
            command:
            - ls
            - /var/ready

레디니스 프로브를 통해 ls /var/ready 명령어를 주기적으로 실행한다. ls 명령어는 파일이 하나라도 존재하면 종료코드 0을 반환하고 이외에는 0이 아닌 값을 반환한다. 따라서 파드가 통신 준비가 되었을 때 어떤 파일이든 /var/ready에 파일을 생성하면 레디니스 프로브가 성공하게 된다.

참고로 레디니스 프로브는 기본 10초마다 실행된다.

레디니스 프로브의 필요성

레디니스 프로브가 정의되어 있지 않으면 파드가 시작되는 즉시 서비스 엔드포인트가 되어 클라이언트의 요청을 수신한다. 준비되지 않은 파드가 요청을 수신하면 "Connection refused" 에러를 받게 된다.

헤드리스 서비스

헤드리스 서비스를 이용하면 DNS 조회로 파드 IP를 찾을 수 있다. clusterIP: None으로 스펙을 정의하면 헤드리스 서비스를 만들게 된다.

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

헤드리스 서비스는 여전히 파드 간 로드밸런싱을 제공하며, 서비스 프록시 대신 DNS 라운드 로빈 기반으로 제공된다.