emeraldgoose / kubernetes_study

Kubernetes In Action
0 stars 0 forks source link

5장. 서비스: 클라이언트가 파드를 검색하고 통신을 가능하게 함 #5

Open emeraldgoose opened 8 months ago

emeraldgoose commented 8 months ago

5.1 서비스 소개

서비스는 파드 그룹에 단일 접점을 만들려고 할 때 생성하는 리소스이다. 클라이언트는 해당 IP와 포트로 접속한 다음 해당 서비스를 지원하는 파드 중 하나로 연결되며 서비스 연결은 서비스 뒷단의 모든 파드로 로드밸런싱된다. kubectl expose 명령을 사용하여 서비스를 생성할 수 있고 매니페스트 파일을 작성할 수도 있다.

apiVersion: v1
kind: Service
metadata:
  name:
spec:
  ports:
  - port: 80 # 서비스가 사용할 포트
    targetPort: 8080 # 서비스가 포워드할 컨테이너 포트
  selector: # key=value 레이블이 있는 모든 파드가 이 서비스에 포함된다.
    key: value

kubectl get svc 명령을 통해 네임스페이스의 모든 서비스 리소스를 조회하고 서비스에 내부 클러스터 IP가 할당됐는지 확인할 수 있다.

몇 가지 방법으로 클러스터 내에서 서비스로 요청을 보내 테스트해볼 수 있다.

특정 클라이언트의 모든 요청을 매번 같은 파드로 리디렉션할 수 있다. 서비스의 세션 어피티니(sessionAffinity) 속성을 기본값 None 대신 ClientIP로 설정하면 서비스 프록시는 동일한 클라이언트 IP의 모든 요청을 동일한 파드로 전달한다.

apiVersion: v1
kind: Service
spec:
  sessionAffinity: ClientIP

서비스는 단일 포트만 노출하지만 여러 포트를 지원할 수도 있다. 예를 들어 파드가 두 개의 포트를 수신한다면 하나의 서비스를 사용해 포트 80과 443을 파드의 포트 8080과 8443으로 전달할 수 있다. 하나의 서비스를 사용해 멀티 포트 서비스를 사용하면 단일 클러스터 IP로 모든 서비스 포트가 노출된다. 여러 포트가 있는 서비스를 만들 때는 각 포트의 이름을 지정해야 한다.

apiVersion: v1
kind: Service
metadata:
  name:
spec:
  ports:
  - name: http # 포트 80은 파드의 포트 8080에 매핑된다.
    port: 80
    targetPort: 8080
  - name: https
    port: 443
    targetPort: 8443
  selector: # 레이블 셀렉터는 항상 모든 서비스에 적용된다.
    key: value

각 파드의 포트에서 이름을 지정하고 서비스 스펙에서 이름으로 참조할 수도 있다. 따라서 포트 번호가 잘 알려진 경우가 아니더라도 서비스 스펙을 좀 더 명확하게 한다. 이렇게 하면 나중에 서비스 스펙을 변경하지 않고 파드 스펙에서만 포트 번호를 변경하면 된다. 추가적으로 서비스 IP에 curl 명령어는 동작하지만 핑은 할 수 없다.

kind: Pod
spec:
  containers:
    - name:
      ports:
      - name: http # 컨테이너 포트 8080은 http라고 한다.
        containerPort: 8080
      - name: https
        containerPort: 8443
apiVersion: v1
kind: Service
spec:
  ports:
  - name: http # 포트 80은 http라는 컨테이너 포트에 매핑된다.
    port: 80
    targetPort: http
  - name: https
    port: 443
    targetPort: https
emeraldgoose commented 8 months ago

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

서비스 기능으로 외부 서비스를 노출하려는 경우가 있을 수 있는데 이 경우 서비스 로드밸런싱과 서비스 검색 모두 활용할 수 있다. 서비스는 파드에 직접 연결되지 않고 엔드포인트 리소스가 사이에 존재한다. 엔드폰인트 리소스는 서비스로 노출되는 파드의 IP 주소와 포트 목록이다. 엔드포인트 리소스는 kubectl get endpoint {service_name} 명령으로 확인할 수 있다. 클라이언트가 서비스에 연결하면 서비스 프록시는 목록 중 하나의 IP와 포트 쌍을 선택하고 들어온 연결을 대상 파드의 수신 대기 서버로 전달한다.

서비스 엔드포인트를 수동으로 구성하려면 셀렉터 없이 서비스를 생성하고 엔드포인트 서비스를 생성해야 한다.

apiVersiono: v1
kind: Service
metadata:
  name:
spec: # 셀렉터가 정의돼 있지 않다
  ports:
  - port: 80
apiVersion: v1
kind: Endpoints
metadata:
  name: # 엔드포인트 오브젝트의 이름은 서비스 이름과 일치해야 한다.
subsets:
  - address: # 서비스가 연결을 전달할 엔드포인트 IP
    - ip: 11.11.11.11
    - ip: 22.22.22.22
    ports: # 엔드포인트의 대상 포트
    - port: 80

서비스와 엔드포인트 리소스가 모두 서버에 게시되면 파드 셀렉터가 있는 일반 서비스처럼 서비스를 사용할 수 있다. 서비스가 만들어진 후 만들어진 컨테이너에는 서비스의 환경변수가 포함되며 IP:포트 쌍에 대한 모든 연결은 서비스 엔드포인트 간에 로드밸런싱한다.

FQDN(Fully Qualified Domain Name, 정규화된 도메인 이름)으로 외부 서비스를 참조할 수 있다. 외부 서비스의 별칭으로 사용되는 서비스를 만들려면 유형(type) 필드를 ExternalName으로 설정해 서비스 리소스를 만든다.

apiVersion: v1
kind: Service
metadata:
  name: external-service
spec:
  type: ExternalName # 서비스 유형이 ExternalName으로 설정된다.
  externalName: someapi.somecompany.com # 실제 서비스의 정규화된 도메인 이름
  ports:
  - port: 80

서비스가 생성되면 파드는 서비스의 FQDN을 사용하는 대신 external-service.default.svc.cluster.local 도메인 이름으로 외부 서비스에 연결할 수 있다. 서비스를 사용하는 파드에서 실제 서비스 이름과 위치가 숨겨져 나중에 externalName 속성을 변경하거나 유형을 다시 ClientIIP로 변경하고 서비스 스펙을 만들어 수정하면 나중에 다른 서비스를 가리킬 수 있다. 서비스를 위한 엔드포인트 오브젝트를 수동 혹은 서비스에 레이블 셀렉터를 지정해 엔드포인트가 자동으로 생성되도록 한다. ExternalName 서비스는 DNS 레벨에서만 구현된다. 서비스에 관한 간단한 CNAME DNS 레코드가 생성된다. 따라서 서비스에 연결하는 클라이언트는 서비스 프록시를 완전히 무시하고 외부 서비스에 직접 연결된다. 이러한 이유로 ExternalName 유형의 서비스는 ClientIP를 얻지 못한다.

emeraldgoose commented 8 months ago

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

특정 서비스를 외부에 노출해 외부 클라이언트가 액세스할 수 있게 하고 싶다면 가능한 방법들이 몇가지 있다.

노드포트 서비스를 만들면 쿠버네티스는 모든 노드에 특정 포트를 할당하고(모든 노드에서 동일한 포트 번호가 사용된다) 서비스를 구성하는 파드로 들어오는 연결을 전달한다. 서비스의 내부 클러스터 IP 뿐만 아니라 모든 노드의 IP와 할당된 노드포트로 서비스에 액세스할 수 있다.

apiVersion: v1
kind: Service
metadata:
  name:
spec:
  type: NodePort
  ports:
  - port: 80 # 서비스 내부 클러스터 IP의 포트
    targetPort: 8080 # 서비스 대상 파드의 포트
    nodePort: 30123 # 각 클러스터 노드의 포트 30123으로 서비스에 액세스할 수 있다.
  selector:
    ...

노드포트를 지정하지 않으면 쿠버네티스가 임의의 포트를 선택한다.

클라우드 공급자(aws, gcp, azure 등)에서 실행되는 쿠버네티스 클러스터는 일반적으로 클라우드 인프라에서 로드밸런서를 자동으로 프로비저닝하는 기능을 제공한다. 노드포트 대신 서비스 유형을 로드밸런서로 설정하기만 하면 된다. 로드밸런서는 액세스 가능한 고유한 IP 주소를 가지고 모든 연결을 서비스로 전달하므로 로드밸런서의 IP주소로 서비스에 접근할 수 있다.

apiVersion: v1
kind: Service
metadata:
  name:
spec:
  type: LoadBalancer # 이 유형의 서비스는 쿠버네티스 클러스터를 호스팅하는 인프라에서 로드밸런서를 얻을 수 있다.
  ports:
  - port: 80
    targetPort: 8080
  selector:
    ...

서비스를 생성한 후 클라우드 인프라가 로드밸런서를 생성하고 IP 주소를 서비스 오브젝트에 쓰는데 시간이 걸린다. 완료되면 로드밸런서 IP 주소가 서비스의 EXTERNAL-IP 주소로 표시된다.

emeraldgoose commented 8 months ago

5.4 인그레스 리소스로 서비스 외부 노출

인그레스(Ingress)는 한 IP 주소로 수십 개의 서비스에 접근이 가능하도록 지원해준다. 클라이언트가 HTTP 요청을 인그레스에 보낼 때, 요청한 호스트와 경로에 따라 요청을 전달할 서비스가 결정된다. 인그레스는 네트워크 스택의 애플리케이션 계층(HTTP)에서 동작하며 서비스가 할 수 없는 쿠키 기반 세션 어피니티 등과 같은 기능을 제공할 수 있다. 인그레스 리소스를 작동시키려면 인그레스 컨트롤러를 실행해야 하는데 일부 쿠버네티스 환경에서는 기본 컨트롤러를 제공하지 않으므로 확인이 필요하다.

http://kubia-example.com 서비스에 액세스하려면 도메인 이름이 인그레스 컨트롤러의 IP오 매핑되도록 확인해야 한다. kubectl get ingresses 명령을 통해 인그레스의 목록을 확인하고 ADDRESS 열에 IP가 표시된다. IP 확인 후 kubia.example.com을 해당 IP로 확인하도록 DNS 서버를 구성하거나 /etc/hosts에 추가할 수 있다.

인그레스는 다음의 방식으로 동작한다.

  1. 클라이언트가 kubia.example.com을 찾는다. DNS 조회를 수행하고 DNS에서 인그레스 컨트롤러의 IP를 반환한다.
  2. 클라이언트는 HTTP 요청을 인그레스 컨트롤러로 전송하고 host 헤더에서 kubia.example.com을 지정한다.
  3. 컨트롤러는 해당 헤더에서 클라이언트가 액세스하려는 서비스를 결정하고 서비스와 관련된 엔드포인트 오브젝트로 파드 IP를 조회한 다음 클라이언트 요청을 파드에 전달한다. 인그레스 컨트롤러는 요청을 서비스로 전달하지 않는다.

하나의 인그레스로 여러 서비스를 노출할 수 있다. 인그레스 스펙은 규칙과 경로가 모두 배열이므로 여러 항목을 가질 수 있다.

...
  - host: kubia.example.com
    http:
      paths:
      - path: /kubia # kubia.example.com/kubia으로의 요청은 kubia 서비스로 라우팅된다
        backend:
          serviceName: kubia
          servicePort: 80
     - path: /bar # kubia.example.com/bar로의 요청은 bar 서비스로 라우팅된다
       backend:
         serviceName: bar
         servicePort: 80

또는 서로 다른 호스트로 서로 다른 서비스를 매핑할 수도 있다.

spec:
  rules:
  - host: foo.example.com # foo.exameple.com으로의 요청은 서비스 foo로 라우팅된다.
    http:
      paths:
      - path: /
        backend:
          serviceName: foo
          servicePort: 80
  - host: bar.example.com # bar.exameple.com으로의 요청은 서비스 bar로 라우팅된다.
    http:
      paths:
      - path: /
        backend:
          serviceName: bar
          servicePort: 80

컨트롤러가 수신한 요청은 요청의 호스트 헤더에 따라 서비스 foo 또는 bar로 전달된다. DNS는 foo.example.com, bar.example.com 도메인 이름을 모두 인그레스 컨트롤러의 IP 주소로 지정해야 한다.

HTTPS 트래픽을 위해 TLS를 지원하도록 인그레스를 설정할 수 있다. 먼저, 개인키와 인증서를 생성한다. openssl genrsa -out tls.key 2048 openssl req -new -x509 -key tls.key -out tls.cert -dyas 360 -subj /CN=kubia.example.com kubectl create secret tls tls-secret --cert=tls.cert --key=tls.key 명령을 통해 두 파일로 시크릿을 만든다.

apiVersion: extension/v1beta1
kind: Ingress
metadata:
  name:
spec:
  tls:
  - hosts:
    - kubia.example.com
    secretName: tls-secret # 개인 키와 인증서는 이전에 작성한 tls-secret을 참조한다.
rules:
  - host: kubia.example.com
    http:
      paths:
      - path: /
        backend:
          serviceName: kubia-nodeport
          servicePort: 80

인그레스를 삭제하고 다시 만드는 대신 kubectl apply -f {manifest}.yaml을 호출하면 파일에 지정된 내용으로 인그레스 리소스가 업데이트된다.

emeraldgoose commented 8 months ago

5.5 파드가 연결을 수락할 준비가 됐을 때 신호 보내기

파드가 요청을 처리할 준비가 되었는지 주기적으로 확인하는 레디니스 프로브(readiness probe)를 정의할 수 있다. 레디니스 프로브는 주기적으로 호출되며 성공을 반환하면 컨테이너가 요청을 수락할 준비가 되었다는 신호다. 레디니스 프로브의 유형은 다음과 같다.

레디니스 프로브는 라이브니스 프로브와 달리 상태 점검에 실패하더라도 컨테이너가 재시작하지 않는다. 대신 레디니스 프로브가 실패한다면 파드는 엔드포인트 오브젝트에서 제거되며 클라이언트 요청이 파드로 전달되지 않는다. 다음의 매니페스트는 파드에 레디니스 프로브를 추가하는 예제이다.

apiVersion: v1
kind: ReplicationController
...
spec:
  ...
  template:
  ...
    spec:
      containers:
      - name: 
        image: 
        readinessProbe: # 파드의 각 컨테이너에 레디니스 프로브가 정의된다
          exec:
            command:
            - ls
            - /var/ready

kubectl get po 명령어로 파드를 조회하면 READY 열에 파드가 준비됐는지 확인할 수 있다.

레디니스 프로브는 항상 정의해야 한다. 레디니스 프로브를 추가하지 않으면 파드가 시작하는 즉시 서비스 엔드포인트가 되기 때문이다. 애플리케이션이 연결을 시작하는데 너무 오래 걸리는 경우 클라이언트의 서비스 요청은 수락할 준비가 되지 않은 파드에 전달되어 클라이언트는 "Connection refused" 에러를 보게 된다. 또한, 레디니스 프로브에 파드의 종료 코드를 포함할 필요가 없다. 쿠버네티스는 파드를 삭제하자마자 모든 서비스에서 파드를 제거하기 때문이다.

emeraldgoose commented 8 months ago

5.6 헤드리스 서비스로 개별 파드 찾기

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

apiVersion: v1
kind: Service
metadata:
  name:
spec:
  clusterIP: None
  ports:
  - port: 80
    targetPort: 8080
  selector:
    key: value

kubectl getkubectl describe로 서비스를 살펴보면 클러스터 IP가 없고 엔드포인트에 파드 셀렉터와 일치하는 파드가 포함돼 있음을 알 수 있다. 파드에 레디니스 프로브가 포함돼 있기 때문에 준비된 파드만 서비스의 엔드포인트로 조회된다.

헤드리스 서비스를 사용하더라도 클라이언트는 일반 서비스와 마찬가지로 서비스의 DNS 이름에 연결해 파드에 연결할 수 있다. 그러나 헤드리스 서비스에서는 DNS가 파드의 IP를 반환하기 때문에 클라이언트는 서비스 프록시 대신 파드에 직접 연결한다. 헤드리스 서비스는 파드간의 로드밸런싱을 제공하지만 서비스 프록시 대신 DNS 라운드로빈 메커니즘을 사용한다.

emeraldgoose commented 8 months ago

5.7 서비스 문제 해결

서비스 파드에 액세스할 수 없는 경우 다음의 내용을 확인해본다.