-
[Kubernetes] 서비스, 로드 밸런스, 네트워킹 - Service공부/데이터 2025. 1. 29. 23:14
배포한 어플리케이션을 노출하는 방법
쿠버네티스에서 애플리케이션을 외부에 노출하는 일반적인 방법은
Service
리소스를 사용하는 것입니다. 하지만 몇 가지 특정한 경우에는 서비스를 사용하지 않고도 애플리케이션을 노출할 수 있습니다. 여기서 소개하는 방식은 전부 단점이 존재하기 때문에 Service를 사용해 애플리케이션을 노출해야 합니다.Port-forwarding
kubectl port-forward
명령어를 사용하여 로컬 머신의 특정 포트를 파드 내부의 컨테이너 포트에 연결합니다. 이렇게 하면 로컬 머신의 포트를 통해 파드 내부에서 실행되는 애플리케이션에 접근할 수 있습니다.포트 포워딩은 개발 및 디버깅 과정에서 주로 사용되며 로컬 머신에만 유효하여 영구적인 애플리케이션 노출 방법으로는 적합하지 않습니다.
kubectl port-forward my-pod 8080:80
위 명령어는
my-pod
파드의 80번 포트를 로컬 머신의 8080번 포트에 연결합니다. 로컬 머신에서localhost:8080
으로 접속하면my-pod
내부의 애플리케이션에 접근할 수 있습니다.HostPort
파드가 실행되는 노드의 특정 포트를 파드 내부의 컨테이너 포트에 직접 매핑합니다. 외부에서는 노드의 IP 주소와 매핑된 포트를 통해 애플리케이션에 접근할 수 있습니다.
간단하게 애플리케이션을 노출할 수 있지만 동일한 노드에서 동일한 포트를 사용하는 여러 파드를 실행할 수 없고 노드의 포트가 직접 노출되므로 보안에 취약할 수 있습니다. 또한 노드가 변경되면 애플리케이션에 접근하는 IP 주소가 변경될 수 있습니다.
HostNetwork
파드가 노드의 네트워크 네임스페이스를 공유합니다. 파드는 노드의 IP 주소를 직접 사용하고, 노드에 할당된 모든 포트에 접근할 수 있습니다.
파드의 네트워크 성능을 향상시킬 수 있지만 파드가 노드의 모든 포트에 접근할 수 있으므로 보안에 취약할 수 있습니다. 또한 노드에서 실행되는 다른 애플리케이션과 포트 충돌이 발생할 수 있고 노드가 변경되면 애플리케이션에 접근하는 IP 주소가 변경될 수 있습니다.
Service
클러스터 내에서 실행 중인 애플리케이션을 하나의 외부 엔드포인트 뒤에 노출시킬 수 있습니다. 이는 워크로드가 여러 백엔드에 분산되어 있는 경우에도 가능합니다. 서비스는 클러스터 내에서 하나 이상의 파드로 실행 중인 네트워크 애플리케이션을 노출시키는 방법입니다.
서비스를 사용하면 애플리케이션 코드에서 별도의 서비스 검색 메커니즘을 사용할 필요가 없습니다. Deployment를 사용하는 경우, 파드가 동적으로 생성 및 파괴되더라도 서비스는 일관된 엔드포인트를 제공합니다. 클라이언트는 서비스를 통해 백엔드 파드의 IP 주소를 직접 관리할 필요가 없습니다.
일반적으로 selector를 통해 서비스가 대상으로 하는 파드 세트를 정의합니다. 서비스는 selector에 일치하는 파드들의 IP 주소와 포트 정보를 엔드포인트로 관리합니다. HTTP 트래픽을 처리하는 경우, Ingress를 사용하여 외부에서 클러스터 내의 워크로드에 접근할 수 있습니다. 아니면 Ingress를 확장한 개념인 Gateway API로 더욱 다양한 네트워크 서비스를 관리할 수 있습니다.
즉 서비스는 다음과 같은 기능을 제공합니다.
- DNS 기반 서비스 발견: 클러스터 내부에서 DNS를 통해 서비스 이름으로 파드에 접근할 수 있도록 합니다.
- 로드 밸런싱: 여러 파드에 들어오는 트래픽을 분산하여 서비스의 가용성과 성능을 향상시킵니다.
- 서비스 디스커버리: 서비스는 서비스 디렉토리 역할을 수행하여 애플리케이션이 다른 서비스를 쉽게 찾을 수 있도록 돕습니다.
클라우드 기반 서비스 검색
애플리케이션에서 서비스 검색을 위해 쿠버네티스 API를 사용할 수 있는 경우, API 서버에 일치하는 EndpointSlices를 쿼리할 수 있습니다. 쿠버네티스는 서비스의 파드 세트가 변경될 때마다 서비스의 EndpointSlices를 업데이트합니다.
- EndpointSlices 리소스는 서비스에 속한 Pod들의 엔드포인트 정보를 제공
네이티브 애플리케이션이 아닌 경우, 쿠버네티스는 애플리케이션과 백엔드 파드 사이에 네트워크 포트 또는 로드밸런서를 배치하는 방법을 제공합니다. 어느 쪽이든 워크로드는 이러한 서비스 검색 메커니즘을 사용하여 연결하려는 대상을 찾을 수 있습니다.
Service 정의
Kubernetes에서 서비스는 객체(Pod, ConfigMap과 동일한 개념)입니다. Kubernetes API를 사용하여 서비스를 생성, 조회, 수정할 수 있습니다. 보통은
kubectl
과 같은 도구를 사용하여 API 호출을 간편하게 수행합니다.TCP 포트 9376을 listen하고 label
app.kubernetes.io/name=MyApp
를 가진 파드 집합이 있다고 가정해봅시다. 다음과 같이 서비스를 정의하여 해당 TCP 리스너를 공개할 수 있습니다.apiVersion: v1 kind: Service metadata: name: my-service spec: selector: app.kubernetes.io/name: MyApp ports: - protocol: TCP port: 80 targetPort: 9376
이 매니페스트를 적용하면 "my-service"라는 이름의 새로운 서비스가 생성됩니다. 이 서비스는 기본적으로 ClusterIP 타입이며,
app.kubernetes.io/name: MyApp
label이 부착된 모든 파드의 9376번 TCP 포트를 대상으로 합니다.쿠버네티스는 이 서비스에 가상 IP 주소 메커니즘에 사용되는 IP 주소(클러스터 IP)를 할당합니다.
해당 서비스의 컨트롤러는 해당 selector와 일치하는 파드를 지속적으로 검색한 다음, 서비스에 대한 EndpointSlices 세트에 필요한 업데이트를 수행합니다.
포트 정의
파드의 포트 정의에는 이름이 있으며 서비스의 targetPort 어트리뷰트에서 이러한 이름을 참조할 수 있습니다. 예를 들어, 다음과 같은 방법으로 서비스의 targetPort를 파드 포트에 바인딩할 수 있습니다
apiVersion: v1 kind: Pod metadata: name: nginx labels: app.kubernetes.io/name: proxy spec: containers: - name: nginx image: nginx:stable ports: - containerPort: 80 name: http-web-svc --- apiVersion: v1 kind: Service metadata: name: nginx-service spec: selector: app.kubernetes.io/name: proxy ports: - name: name-of-service-port protocol: TCP port: 80 targetPort: http-web-svc
단일 구성된 이름을 사용하여 서비스에 포함된 파드 간에 포트 번호가 다르더라도 동일한 네트워크 프로토콜을 사용하는 경우에도 이 방식은 작동합니다. 이는 서비스를 배포하고 발전시킬 때 많은 유연성을 제공합니다. 예를 들어, 백엔드 소프트웨어의 다음 버전에서 파드가 노출하는 포트 번호를 변경하더라도 클라이언트에 영향을 미치지 않습니다.
서비스의 기본 프로토콜은 TCP이며 지원되는 다른 프로토콜을 사용할 수도 있습니다.
많은 서비스가 둘 이상의 포트를 노출해야 하기 때문에 쿠버네티스는 단일 서비스에 대해 여러 포트 정의를 지원합니다. 각 포트 정의는 동일한 프로토콜을 가질 수도 있고 다른 프로토콜을 가질 수도 있습니다.
selector가 없는 서비스
서비스는 일반적으로 selector를 통해 Kubernetes 파드에 대한 접근을 추상화하지만, EndpointSlices 객체와 함께 사용되고 selector 없이 정의될 경우, 클러스터 외부에서 실행되는 백엔드를 포함하여 다른 종류의 백엔드를 추상화할 수 있습니다.
다음은 그 예시입니다.
- 프로덕션 환경에서는 외부 데이터베이스 클러스터를 사용하고, 테스트 환경에서는 자체 데이터베이스를 사용하려는 경우
- 다른 네임스페이스 또는 다른 클러스터에 있는 서비스를 가리키도록 서비스를 구성하려는 경우
- 워크로드를 Kubernetes로 마이그레이션하는 동안 일부 백엔드만 Kubernetes에서 실행하는 경우
이러한 시나리오에서는 파드와 일치하는 selector를 지정하지 않고 서비스를 정의할 수 있습니다.
apiVersion: v1 kind: Service metadata: name: my-service spec: ports: - name: http protocol: TCP port: 80 targetPort: 9376
이 서비스에는 selector가 없으므로 해당 EndpointSlice(및 레거시 엔드포인트) 객체가 자동으로 생성되지 않습니다. EndpointSlice 객체를 수동으로 추가하여 서비스를 실행 중인 네트워크 주소 및 포트에 매핑할 수 있습니다.
apiVersion: discovery.k8s.io/v1 kind: EndpointSlice metadata: name: my-service-1 # by convention, use the name of the Service # as a prefix for the name of the EndpointSlice labels: # You should set the "kubernetes.io/service-name" label. # Set its value to match the name of the Service kubernetes.io/service-name: my-service addressType: IPv4 ports: - name: http # should match with the name of the service port defined above appProtocol: http protocol: TCP port: 9376 endpoints: - addresses: - "10.4.5.6" - addresses: - "10.1.2.3"
커스텀 EndpointSlices
서비스는 일반적으로 selector를 통해 Kubernetes 파드에 연결됩니다. 하지만 selector 없이도 서비스를 정의할 수 있으며, 이 경우에는 엔드포인트 슬라이스(EndpointSlice) 객체를 사용하여 서비스가 연결할 대상을 명시적으로 지정합니다.
사용자가 직접 생성한 또는 코드를 통해 생성한 EndpointSlice 객체에는
endpointslice.kubernetes.io/managed-by
label을 반드시 설정해야 합니다. 이 label은 EndpointSlice를 관리하는 컨트롤러를 식별하는 데 사용됩니다.label 값 지정 방법은 다음과 같습니다.
- 사용자 정의 컨트롤러: 사용자가 직접 EndpointSlice를 관리하는 컨트롤러를 개발한 경우 레이블 값을 "my-domain.example/name-of-controller"와 같은 형식으로 지정하는 것이 좋습니다.
- 외부 도구 사용: 외부 도구를 사용하여 EndpointSlice를 관리하는 경우 도구 이름을 모두 소문자로 변환하고 공백 및 구분 기호를 하이픈(-)으로 바꾸어 레이블 값으로 사용합니다. (예: my-endpoint-management-tool)
- 직접 관리:
kubectl
과 같은 도구를 사용하여 직접 EndpointSlice를 관리하는 경우 "staff" 또는 "cluster-admins"와 같은 레이블 값을 사용하여 수동 관리임을 나타냅니다.
예약된 label 값인 "controller"는 사용하지 않도록 합니다. 이 값은 Kubernetes 컨트롤 플레인에서 직접 관리하는 EndpointSlice를 식별하는 데 사용됩니다.
selector 없이 서비스에 액세스하기
selector 없이 서비스를 사용하는 경우, 서비스는 EndpointSlice 객체에 정의된 엔드포인트 정보를 기반으로 트래픽을 라우팅합니다.
만약 selector 없이 정의된 서비스가 있고 해당 서비스의 EndpointSlice에 10.1.2.3:9376과 10.4.5.6:9376 두 개의 엔드포인트가 있다면, 이 서비스로 들어오는 트래픽은 두 엔드포인트 중 하나로 무작위로 분산됩니다. 즉, selector를 사용하지 않더라도 EndpointSlice를 통해 서비스에 연결할 대상을 지정할 수 있습니다.
selector 없이 서비스를 사용하는 이유는 첫 번째로 유연성입니다. selector 없이 서비스를 사용하면 파드를 직접 관리하지 않고, EndpointSlice를 통해 서비스를 구성할 수 있습니다. 두 번째로 복잡한 시나리오입니다. 다양한 환경에 분산된 서비스를 하나의 서비스로 통합하거나 외부 서비스를 연결하는 등 복잡한 시나리오에서 유용합니다. 마지막으로 Legacy 시스템 연동입니다. 기존 시스템과의 연동을 위해 selector 대신 EndpointSlice를 사용할 수 있습니다. 단 이럴 경우, EndpointSlice를 직접 관리해야 하므로 서비스 구성이 복잡해질 수 있습니다.
ExternalName 서비스는 selector가 없고 대신 DNS 이름을 사용하는 서비스의 특수한 경우입니다.
멀티포트 서비스
일부 서비스에서는 여러 개의 포트를 노출해야 할 수 있습니다. Kubernetes에서는 서비스 객체에 여러 개의 포트 정의를 구성할 수 있습니다. 서비스에 여러 개의 포트를 사용하는 경우, 모든 포트에 이름을 지정하여 명확하게 구분해야 합니다.
apiVersion: v1 kind: Service metadata: name: my-service spec: selector: app.kubernetes.io/name: MyApp ports: - name: http protocol: TCP port: 80 targetPort: 9376 - name: https protocol: TCP port: 443 targetPort: 9377
서비스 유형
애플리케이션의 일부 부분(예: 프론트엔드)의 경우 클러스터 외부에서 액세스할 수 있는 외부 IP 주소에 서비스를 노출하고 싶을 수 있습니다. 쿠버네티스 서비스 유형을 사용하면 원하는 서비스 종류를 지정할 수 있습니다. 사용 가능한 type과 그 동작은 다음과 같습니다.
- ClusterIP (기본값): 서비스를 클러스터 내부 IP 주소로만 노출합니다. 이 유형은 클러스터 외부에서는 접근할 수 없습니다. Ingress 또는 Gateway를 사용하여 외부 인터넷에 공개할 수 있습니다.
- NodePort: 각 노드의 IP 주소에 고정 포트(NodePort)를 사용하여 서비스를 노출합니다. Kubernetes는 NodePort를 사용할 수 있도록 클러스터 IP 주소를 설정합니다. 이는 ClusterIP 유형 서비스를 요청한 것과 동일합니다.
- LoadBalancer: 외부 로드 밸런서를 사용하여 서비스를 외부에 노출합니다. Kubernetes 자체는 로드 밸런싱 기능을 제공하지 않으므로 별도로 로드 밸런서를 준비하거나 클라우드 제공자와 통합해야 합니다.
- ExternalName: 서비스를
externalName
필드의 내용(예: 호스트명api.foo.bar.example
)에 매핑합니다. 이 매핑은 클러스터의 DNS 서버가 해당 외부 호스트명 값을 반환하는 CNAME 레코드를 설정하도록 구성합니다. 별도의 프록시 설정은 없습니다.
서비스 유형 필드(
type
)는 중첩적인 기능을 가지고 있으며 각 레벨은 이전 레벨의 기능을 추가합니다. 하지만 이 중첩적 설계에는 한 가지 예외가 있는데NodePort
할당을 비활성화하여 LoadBalancer 서비스를 정의할 수 있습니다.type: ClusterIP
ClusterIP 유형은 클러스터에서 해당 목적으로 예약된 IP 주소 풀에서 IP 주소를 할당합니다. 다른 여러 서비스 유형은 ClusterIP 유형을 기반으로 구축됩니다. 서비스의
spec.clusterIP
를 "None"으로 설정하면 Kubernetes는 IP 주소를 할당하지 않습니다. 이러한 서비스를 헤드리스 서비스라고 합니다.즉, 클러스터 안의 파드끼리 통신하기 위한 private IP를 할당하고 외부에선 접근이 불가능하며 L4 계층의 로드밸런싱이 이루어집니다.
서비스 디버깅 또는 노트북에서 직접 서비스에 연결하거나 내부 트래픽 허용, 내부 대시보드 표시에서 주로 사용합니다.
자신의 IP 주소 선택
서비스 생성 요청 시 사용자 지정 클러스터 IP 주소를 지정할 수 있습니다. 이를 위해서는
spec.clusterIP
필드에 원하는 IP 주소를 설정합니다. 예를 들어, 기존에 사용하던 DNS 엔트리와 일치하는 IP 주소를 사용해야 하는 경우나 기존 시스템이 특정 IP 주소에 연결되어 있고, 재구성이 어려운 경우입니다.지정하는 IP 주소는 API 서버에 구성된
service-cluster-ip-range
CIDR 범위 내에 있어야 합니다. 유효하지 않은 IP 주소를 지정하면 API 서버에서 422 HTTP 상태 코드를 반환하여 오류를 알립니다.Kubernetes는 서비스 간 IP 주소 중복을 최소화하기 위한 메커니즘을 제공합니다.
type: NodePort
NodePort 서비스 유형은 각 노드의 지정된 포트를 통해 외부에서 서비스에 접근할 수 있도록 합니다. Kubernetes 컨트롤 플레인은
--service-node-port-range
플래그(기본값: 30000-32767)에서 지정된 범위 내에서 고유한 포트 번호를 할당하고 각 노드는 할당된 포트 번호를 수신 대기하도록 설정됩니다. 수신이 될 경우, 노드는 수신된 트래픽을 서비스에 연결된 파드 중 하나로 전달합니다. 클러스터 외부에서 해당 노드의 IP 주소와 할당된 포트 번호를 사용하여 서비스에 접근할 수 있습니다. 클러스터 내부로 들어온 트래픽을 특정 파드로 연결하기 위한 ClusterIP는 자동으로 생성됩니다.NodePort의 장점으로 자체 로드 밸런싱 솔루션을 자유롭게 설정하고 Kubernetes에서 완전히 지원하지 않는 환경을 구성하거나 하나 이상의 노드의 IP 주소를 직접 노출할 수 있습니다.
단점으로는 NodePort는 각 노드에 고정된 포트를 사용하므로 노드의 IP 주소와 포트 번호를 알아야 하며 포트당 1개의 서비스만 할당이 가능합니다. 또한 노드의 IP 주소는 동적으로 변경될 수 있으므로 안정적인 접근을 위해서는 별도의 로드 밸런싱 솔루션을 사용해야 합니다.
단점이 많기 때문에 운영 환경에서는 주로 사용하지 않습니다.
apiVersion: v1 kind: Service metadata: name: my-service spec: type: NodePort selector: app.kubernetes.io/name: MyApp ports: - port: 80 # By default and for convenience, the `targetPort` is set to # the same value as the `port` field. targetPort: 80 # Optional field # By default and for convenience, the Kubernetes control plane # will allocate a port from a range (default: 30000-32767) nodePort: 30007
type: LoadBalancer
클라우드 제공업체에서 외부 로드 밸런서를 지원하는 경우,
type
필드를LoadBalancer
로 설정하면 서비스에 대한 로드 밸런서가 프로비저닝됩니다. 로드 밸런서의 실제 생성은 비동기적으로 수행되며 프로비저닝된 로드 밸런서에 대한 정보는 서비스의status.loadBalancer
필드에 게시됩니다.apiVersion: v1 kind: Service metadata: name: my-service spec: selector: app.kubernetes.io/name: MyApp ports: - protocol: TCP port: 80 targetPort: 9376 clusterIP: 10.0.171.239 type: LoadBalancer status: loadBalancer: ingress: - ip: 192.0.2.127
외부 로드 밸런서로부터의 트래픽은 백엔드 파드로 전달됩니다. 로드 밸런싱 방식은 클라우드 제공업체에 따라 결정됩니다.
type: LoadBalancer
인 서비스를 구현하기 위해 Kubernetes는 일반적으로type: NodePort
인 서비스를 요청하는 것과 동일한 변경 사항을 먼저 적용합니다. 그런 다음, cloud-controller-manager 컴포넌트가 외부 로드 밸런서를 구성하여 할당된 노드 포트로 트래픽을 전달하도록 설정합니다.클라우드 제공업체에서 지원하는 경우, 노드 포트 할당을 생략하고 로드 밸런스된 서비스를 구성할 수 있습니다.
해당 유형은 필터링, 라우팅이 불가하며 기본적으로 NodePort를 확장시킨 개념으로 NodePort의 단점을 그대로 가지게 됩니다.
서비스의 load balance를 사용하지 않고 GCP의 Cloud Load Balancing, AWS의 ELB, ALB를 연결해도 될까?
외부 로드 밸런서 서비스를 활용하여 Kubernetes 서비스에 대한 트래픽 분산을 처리할 수 있습니다.
클라우드 제공업체의 로드 밸런서는 Kubernetes의 기본적인 로드 밸런싱 기능을 넘어서는 다양한 고급 기능을 제공합니다. 예를 들어, HTTP(S) 로드 밸런싱, SSL 오프로딩, 건강 검사, 자동 확장 등을 지원합니다. 또한 Kubernetes의 제약 없이 로드 밸런서를 자유롭게 구성하고 관리할 수 있습니다.
단점으로는 Kubernetes 서비스와 외부 로드 밸런서를 함께 관리해야 하므로 구성이 복잡해질 수 있고 클라우드 제공업체의 로드 밸런서 사용에 따른 추가 비용이 발생할 수 있습니다.
또한 Kubernetes 클러스터와 외부 로드 밸런서 간의 네트워크 연결을 올바르게 설정해야 하며 로드 밸런서의 헬스 체크 설정을 Kubernetes 서비스의 상태와 일치하도록 구성하고 보안 설정을 철저히 해야 합니다.
type: ExternalName
ExternalName 타입의 서비스는 일반적인 selector(예: my-service, cassandra) 대신 DNS 이름에 서비스를 연결하는 방식입니다. 이러한 서비스는
spec.externalName
매개변수를 사용하여 설정합니다.apiVersion: v1 kind: Service metadata: name: my-service namespace: prod spec: type: ExternalName externalName: my.database.example.com
위 예시에서는
my-service
이라는 서비스를 정의하여prod
네임스페이스의my-service
서비스를my.database.example.com
DNS 이름에 연결합니다.사용자가
my-service.prod.svc.cluster.local
호스트를 조회하면 클러스터 DNS 서비스는 다음과 같은 방식으로 응답합니다.- CNAME 레코드 반환: 클러스터 DNS 서비스는
my-service.prod.svc.cluster.local
에 대한 CNAME 레코드를 반환합니다. 이 CNAME 레코드의 값은my.database.example.com
입니다. - DNS 레벨 리다이렉션: 사용자는 실제로
my-service.prod.svc.cluster.local
에 접근하는 것이 아니라, CNAME 레코드에 의해my.database.example.com
주소로 리다이렉션됩니다. - ExternalName 서비스의 역할: 이러한 리다이렉션은 외부 리소스(이 경우
my.database.example.com
)에 대한 DNS 레벨에서 일어납니다. 즉, Kubernetes 서비스 자체가 프록싱이나 포워딩을 하지 않고 DNS 레코드를 통해 대상 주소를 알려주는 방식입니다.
헤드리스 서비스
일부 경우에는 로드 밸런싱이나 단일 서비스 IP가 필요하지 않을 수 있습니다. 이러한 경우
spec.clusterIP
를 "None"으로 명시적으로 설정하여 헤드리스 서비스를 생성할 수 있습니다.헤드리스 서비스의 특징은 다음과 같습니다.
- 로드 밸런싱 없음: 헤드리스 서비스는 Kubernetes의 로드 밸런싱 기능을 사용하지 않습니다.
- kube-proxy 미사용: kube-proxy는 헤드리스 서비스에 대해 트래픽을 처리하지 않습니다.
- 직접 연결: 클라이언트는 서비스에 연결할 Pod를 직접 선택하여 연결할 수 있습니다.
- DNS 기반 서비스 발견: 헤드리스 서비스는 클러스터 내부의 DNS 서비스를 통해 각 Pod의 IP 주소를 제공합니다.
spec.clusterIP
를 생략하는 것은 "None"으로 설정하는 것과 다르므로 생략하면 안됩니다.DNS 구성은 selector의 여부에 따라 달라집니다.
- selector가 있는 경우: 서비스의 선택기에 일치하는 모든 Pod의 IP 주소가 DNS 레코드에 등록됩니다.
- selector가 없는 경우: EndpointSlice를 사용하여 서비스가 연결할 대상을 지정해야 합니다.
헤드리스 서비스는 서비스 메시 패턴에서 서비스 간의 직접적인 통신을 구현할 때나 특정 Pod에 직접 연결해야 하는 경우에 사용됩니다.
레퍼런스
https://kubernetes.io/docs/concepts/services-networking/service/