[IT-정보] 서비스 개념 완벽 가이드 – 쿠버네티스 네트워크 연결의 핵심 원리

서비스 관련 쿠버네티스 구조를 설명하는 대표 이미지

끊임없이 변하는 포드 주소, 어떻게 관리하시나요?

어제까지 잘 작동하던 CI/CD 파이프라인이 갑자기 멈췄다는 알림을 받았어요. 로그를 살펴보니 프론트엔드 서비스가 백엔드 서버에 접속하지 못하고 Connection Refused 에러를 뱉고 있네요. 원인을 찾아보니 백엔드 포드가 재시작되면서 기존에 사용하던 IP 주소가 완전히 바뀌어 버린 것이 문제였어요.

이런 상황은 쿠버네티스 환경을 처음 접하는 엔지니어라면 누구나 한 번쯤 겪게 되는 아주 흔한 재앙이에요. 쿠버네티스의 기본 단위인 포드(Pod)는 생명 주기가 매우 짧고, 죽었다가 다시 살아날 때마다 새로운 IP를 할당받기 때문이에요. 만약 우리가 매번 바뀌는 포드의 IP를 일일이 설정 파일에 업데이트해야 한다면, 클러스터 규모가 커질수록 운영은 불가능한 수준이 될 거예요.

그래서 우리에게는 변하지 않는 고정된 통로가 필요해요. 포드가 수백 번 죽고 살아나더라도, 외부나 내부의 다른 서비스들이 항상 같은 이름과 주소로 접속할 수 있게 해주는 안정적인 지지대가 바로 서비스(Service)예요. 서비스를 제대로 이해하지 못하면 안정적인 마이크로서비스 아키텍처를 구축하는 것은 꿈도 꿀 수 없어요.

이 글에서는 단순히 정의를 나열하는 것을 넘어, 실무에서 바로 활용할 수 있는 실질적인 지식을 전달할게요. 다음 내용을 통해 서비스의 본질을 파악할 수 있어요.

  • 포드의 휘발성을 극복하는 서비스의 존재 이유
  • 클러스터 내부에서 트래픽이 흐르는 실제 경로
  • 상황에 맞는 서비스 타입 선택 기준
  • 실무에서 자주 발생하는 네트워크 설정 오류 해결법

서비스 구축 전 반드시 짚고 넘어가야 할 기초 지식

서비스를 설정하기 전에 우리가 제어하려는 대상이 무엇인지 정확히 알아야 해요. 서비스는 스스로 동작하는 개체가 아니라, 특정 조건에 맞는 포드들을 하나로 묶어주는 논리적인 추상화 계층이에요. 즉, 서비스는 포드라는 실제 일꾼들에게 명령을 전달하는 ‘안내 데스크’와 같다고 생각하면 이해가 빨라요.

서비스가 어떤 포드에 트래픽을 보낼지 결정하는 가장 중요한 기준은 셀렉터(Selector)예요. 셀렉터는 특정 라벨(Label)을 가진 포드들을 찾아내어 서비스의 관리 대상으로 등록해요. 예를 들어, 모든 포드에 `app: backend`라는 라벨이 붙어 있다면, 서비스는 이 라벨을 보고 “아, 이 포드들이 내가 관리할 대상이구나”라고 판단해요.

💡 알아두기
서비스를 생성할 때 가장 많이 실수하는 부분이 라벨 불일치예요. 포드에 설정된 라벨과 서비스의 셀렉터가 글자 하나라도 다르면, 서비스는 연결할 포드를 찾지 못해 트래픽을 허공에 날려버려요. 항상 `kubectl get pods –show-labels` 명령어로 라벨을 확인하는 습관을 가져야 해요.

그렇다면 어떤 타입의 서비스를 선택해야 할까요? 클러스터 내부에서만 통신할 것인지, 아니면 외부 사용자에게 서비스를 공개할 것인지에 따라 결정 기준이 완전히 달라져요. 아래 표를 통해 상황별 최적의 선택지를 비교해 보세요.

서비스 타입 주요 용도 접근 범위 권장 시나리오
ClusterIP 내부 통신용 클러스터 내부 전용 DB, 캐시 서버 등
NodePort 외부 노출용 노드 IP + 특정 포트 테스트 및 간단한 외부 연결
LoadBalancer 클라우드 연동 클라우드 로드밸런서 IP 실제 운영 중인 웹 서비스
ExternalName 외부 주소 매핑 외부 DNS 이름 클러스터 밖의 DB 연결

이러한 기준을 바탕으로 이제 실제 서비스가 어떻게 구성되고 동작하는지 단계별로 깊이 있게 파고들어 볼게요.

서비스 구조와 동작 원리: 5단계 마스터하기

서비스가 단순히 IP를 하나 부여하는 것 이상의 복잡한 매커니즘을 가지고 있다는 사실을 아는 것이 중요해요. 네트워크 패킷이 어떻게 흐르는지 이해해야 트러블슈팅이 가능하니까요. 단계별로 상세히 살펴볼게요.

STEP 1. 라벨과 셀렉터를 이용한 엔드포인트 자동 생성

서비스의 가장 첫 번째 임무는 관리 대상인 포드를 식별하는 것이에요. 사용자가 서비스를 정의할 때 Selector를 지정하면, 쿠버네티스 컨트롤 플레인은 해당 셀렉터와 일치하는 라벨을 가진 모든 포드를 추적해요. 이때 서비스는 내부적으로 Endpoints라는 별도의 객체를 생성하거나 업데이트해요.

Endpoints 객체는 현재 서비스에 연결된 포드들의 실제 IP 주소와 포트 번호 목록을 담고 있는 명단이에요. 만약 어떤 포드가 죽거나 상태 검사(Health Check)에 실패하여 Unready 상태가 되면, 쿠버네티스는 자동으로 그 포드의 IP를 Endpoints 목록에서 삭제해요. 덕분에 클라이언트는 살아있는 포드에게만 안전하게 요청을 보낼 수 있는 것이죠. 이 과정이 자동으로 이루어지기 때문에 우리는 포드의 생사 여부를 신경 쓰지 않고 서비스 이름만 호출하면 돼요.

STEP 2. ClusterIP를 통한 클러스터 내부 통신 최적화

가장 기본이 되는 ClusterIP는 클러스터 내부에서만 유효한 가상 IP를 생성해요. 이 IP는 실제 물리적인 네트워크 인터페이스에 할당된 것이 아니라, 쿠버네티스의 네트워크 플러그인과 kube-proxy라는 컴포넌트에 의해 구현되는 논리적인 주소예요.

트래픽 흐름을 자세히 들여다보면 꽤 흥미로워요. 클라이언트 포드가 ClusterIP로 요청을 보내면, 해당 노드에서 실행 중인 kube-proxy가 이 요청을 가로채요. kube-proxy는 미리 설정해 둔 iptables 또는 IPVS 규칙을 확인한 뒤, 요청의 목적지 IP를 실제 포드의 IP 중 하나로 변환(DNAT)하여 전달해요. 이 과정에서 트래픽은 여러 포드 사이로 균등하게 분산(Load Balancing)돼요. 따라서 별도의 복잡한 로드밸런서 설정 없이도 내부 서비스 간의 부하 분산이 자연스럽게 이루어지는 구조예요.

STEP 3. NodePort로 외부 세계와 통로 만들기

클러스터 내부에서만 도는 서비스를 외부에서 접속하게 하려면 NodePort 타입이 필요해요. NodePort는 클러스터의 모든 노드(Node)에 특정 포트 범위(기본 30000~32767)를 개방하여, 어떤 노드의 IP로든 해당 포트로 접속하면 서비스로 연결되도록 설정하는 방식이에요.

예를 들어, 서비스의 NodePort를 31000으로 설정했다면, 클러스터 노드 A의 IP가 192.168.1.10일 때 192.168.1.10:31000으로 접속하면 내부 서비스에 도달할 수 있어요. 하지만 주의할 점이 있어요. 노드의 IP가 바뀌거나 노드가 다운되면 접속이 끊길 수 있다는 점이에요. 또한, 노드의 모든 포트가 외부에 노출되므로 보안 정책을 매우 꼼꼼하게 세워야 해요. 보통 개발 환경이나 간단한 테스트용으로 적합해요.

STEP 4. LoadBalancer를 이용한 프로페셔널한 외부 노출

실제 운영 환경에서는 NodePort만으로는 부족해요. 서비스의 IP가 노드에 종속되는 한계가 있기 때문이죠. 이때 사용하는 것이 LoadBalancer 타입이에요. AWS, GCP, Azure 같은 클라우드 환경을 사용 중이라면, 이 서비스를 생성하는 순간 클라우드 제공업체의 고유한 로드밸런서(예: AWS의 NLB 또는 ELB)가 자동으로 프로비저닝돼요.

이 로드밸런서는 고유한 외부 IP를 가지며, 클러스터 외부에서 들어오는 방대한 트래픽을 안정적으로 받아 각 노드의 NodePort로 전달해 줘요. 사용자는 노드의 IP를 알 필요 없이 클라우드 로드밸런서의 IP 하나만 알면 돼요. 비용은 발생하지만, 확장성과 안정성 측면에서 운영 환경의 표준이라고 할 수 있어요. 트래픽이 폭증하더라도 클라우드 인프라가 알아서 대응해 주므로 데브옵스 엔지니어의 운영 부담을 획기적으로 줄여줘요.

STEP 5. 특수 목적을 위한 Headless Service 활용하기

때로는 로드밸런싱이 필요 없는 경우가 있어요. 예를 들어, 데이터베이스 클러스터처럼 각 포드마다 개별적인 식별이 필요한 경우죠. 이때 사용하는 것이 Headless Service예요. 서비스 설정에서 clusterIP: None으로 지정하면, 서비스는 가상 IP를 할당하지 않아요.

대신, 서비스 이름을 DNS로 조회했을 때 로드밸런싱된 IP가 아닌, 연결된 포드들의 실제 IP 리스트를 직접 반환해요. 이를 통해 클라이언트는 서비스에 의존하지 않고 직접 특정 포드의 IP를 선택하여 통신할 수 있어요. StatefulSet과 함께 사용하면 데이터베이스의 마스터-슬레이브 구조를 관리하는 데 매우 강력한 도구가 돼요.

💡 알아두기
실무에서 서비스 설정을 검증할 때는 반드시 kubectl get endpoints [서비스이름] 명령어를 실행해 보세요. 만약 ENDPOINTS 항목이 <empty>로 나온다면, 서비스는 존재하지만 연결된 포드가 하나도 없다는 뜻이므로 셀렉터 설정을 즉시 재검토해야 해요.

이제 이론을 넘어 실제 설정 예시를 통해 어떻게 코드로 구현되는지 확인해 볼까요?

[실전 예제] 간단한 웹 서버 서비스 설정 시나리오

다음은 `nginx` 포드를 대상으로 하는 ClusterIP 서비스와 NodePort 서비스의 YAML 설정 예시예요. 이 예시를 통해 포트 번호가 어떻게 매핑되는지 눈여겨보세요.

# 1. ClusterIP 서비스 예시
apiVersion: v1
kind: Service
metadata:
  name: my-internal-service
spec:
  type: ClusterIP
  selector:
    app: web-server
  ports:
    - protocol: TCP
      port: 80          # 서비스가 노출하는 포트
      targetPort: 80    # 실제 포드가 듣고 있는 포트

--- 

# 2. NodePort 서비스 예시
apiVersion: v1
kind: Service
metadata:
  name: my-external-service
spec:
  type: NodePort
  selector:
    app: web-server
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
      nodePort: 31000 # 외부에서 접속할 포트 (30000-32767)

여기서 핵심은 porttargetPort의 구분이에요. port는 서비스 자체의 포트이고, targetPort는 트래픽이 최종적으로 도달할 포드의 포트예요. 이 둘을 혼동하면 트래픽이 엉뚱한 곳으로 흘러가게 되니 주의가 필요해요.

자주 하는 실수와 해결법 + FAQ

서비스를 구축하다 보면 설정은 완벽해 보이는데 트래픽이 흐르지 않는 답답한 상황을 자주 마주하게 돼요. 실제 현장에서 가장 빈번하게 발생하는 오류들을 정리했어요.

자주 하는 실수와 해결법

  • 실수: 셀렉터와 라벨의 불일치
    왜 발생하는가: 포드에 설정한 `app: nginx` 라벨과 서비스의 `selector: app: nginx-server`가 서로 달라 포드를 찾지 못함.
    해결법: `kubectl get pods –show-labels`로 포드의 정확한 라벨을 확인하고, 서비스의 selector와 글자 하나까지 일치시키세요.
  • 실수: targetPort 설정 오류
    왜 발생하는가: 포드가 8080 포트에서 동작 중인데, 서비스의 targetPort를 80으로 설정함.
    해결법: 애플리케이션 코드나 Dockerfile에서 정의한 실제 Listen 포트 번호를 targetPort에 정확히 기입하세요.
  • 실수: 네임스페이스(Namespace) 누락
    왜 발생하는가: 포드는 `prod` 네임스페이스에 있는데, 서비스는 `default` 네임스페이스에 생성함.
    해결법: 서비스와 포드는 반드시 같은 네임스페이스에 있어야 서로를 찾을 수 있어요. 생성 시 `metadata.namespace`를 반드시 확인하세요.
  • 실수: 보안 그룹 및 방화벽 차단
    왜 발생하는가: NodePort를 열었지만, 클라우드 인프라(AWS Security Group 등)에서 해당 포트를 허용하지 않음.
    해결법: 클라우드 콘솔에서 노드 그룹의 인바운드 규칙에 해당 NodePort 범위를 허용하도록 설정하세요.
  • 실수: 포드가 Ready 상태가 아님
    왜 발생하는가: 포드는 떠 있지만, Liveness/Readiness Probe가 실패하여 Endpoints 목록에서 제외됨.
    해결법: `kubectl describe pod [포드이름]`으로 프로브 실패 여부를 확인하고 애플리케이션 로그를 점검하세요.
⚠️ 주의
서비스의 IP는 영구적이지 않아요. 서비스 객체 자체가 삭제되면 기존의 ClusterIP도 사라집니다. 설정을 코드(IaC)로 관리하여 재생성 시에도 동일한 설정을 유지하도록 하세요.

자주 묻는 질문

Q. 서비스의 IP 주소는 어떻게 바뀌나요?

A. 서비스 객체를 삭제하고 다시 생성하면 새로운 IP가 할당돼요. 하지만 서비스 이름(DNS)은 유지되므로, 클라이언트가 IP가 아닌 서비스 이름을 사용하도록 설계했다면 코드 변경 없이 대응할 수 있어요.

Q. ClusterIP를 사용하면 외부에서 아예 접속을 못 하나요?
A. 네, 맞아요. ClusterIP는 클러스터 내부 네트워크망 안에서만 통신이 가능하도록 설계된 주소예요. 외부 접속이 필요하다면 반드시 NodePort나 LoadBalancer 타입을 사용해야 해요.

Q. 포트 번호는 왜 30000번대부터 써야 하나요?
A. 쿠버네티스는 기본적으로 시스템에서 사용하는 다른 포트들과 충돌을 피하기 위해 NodePort의 범위를 30000~32767로 제한해 두었어요. 이 범위를 벗어나면 서비스가 정상적으로 생성되지 않아요.

Q. 서비스 하나에 여러 개의 포트를 설정할 수 있나요?
A. 네, 가능해요. 하나의 서비스 객체 내 `ports` 리스트에 여러 개의 포트 정의를 추가하여, HTTP(80)와 HTTPS(443)를 동시에 처리하는 식의 구성이 가능해요.

Q. 서비스의 동작 원리가 iptables라고 하셨는데, IPVS는 무엇인가요?
A. iptables는 규칙이 많아질수록 성능이 저하되는 단점이 있어요. 규모가 아주 큰 클러스터에서는 더 빠른 성능과 효율적인 로드밸런싱을 위해 커널의 IPVS(IP Virtual Server) 기술을 사용하는 것을 권장해요.

안정적인 네트워크를 위한 마지막 체크리스트

쿠버네티스 서비스는 단순히 IP를 연결해 주는 도구가 아니라, 변화무쌍한 컨테이너 환경에서 통신의 일관성을 보장해 주는 핵심 엔진이에요. 오늘 배운 내용을 바탕으로 여러분의 클러스터를 다시 한번 점검해 보세요.

✅ 핵심 요약

  • 포드의 유동적인 IP를 극복하기 위해 서비스(Service)는 필수예요.
  • 셀렉터(Selector)와 라벨(Label)의 일치는 서비스 동작의 전제 조건이에요.
  • 내부 통신은 ClusterIP, 외부 노출은 NodePort나 LoadBalancer를 선택하세요.
  • 서비스의 엔드포인트(Endpoints) 상태를 통해 연결 여부를 즉시 확인할 수 있어요.
  • 포트 설정 시 port와 targetPort의 역할을 명확히 구분해야 해요.
  • 대규모 환경에서는 성능을 위해 IPVS 사용을 고려해 보세요.

이제 이론적인 준비는 끝났어요. 다음 단계로 나아가기 위해 아래의 액션 플랜을 따라 해 보세요.

  • 오늘 할 일: 현재 운영 중인 클러스터에서 서비스의 Endpoints 목록을 확인하고, 실제 포드 IP와 일치하는지 체크해 보세요.
  • 이번 주 할 일: ClusterIP 서비스에 NodePort를 결합하거나, 클라우드 환경에서 LoadBalancer 타입을 생성하여 외부 통신 테스트를 완료해 보세요.
  • 실행 직전 할 일: 서비스 설정 파일을 YAML 형태로 작성하여 Git에 저장하고, 인프라 변경 사항을 관리하는 연습을 시작해 보세요.

이 내용이 여러분의 데브옵스 여정에 실질적인 도움이 되었기를 바라요. 실습 환경에서 직접 적용해 보다가 막히는 부분이 생기면 주저하지 말고 댓글로 질문을 남겨 주세요. 함께 고민해 드릴게요!

함께 읽으면 좋은 글: 쿠버네티스 클러스터 구축 입문 가이드, 쿠버네티스 서비스 기본 개념 심화편

댓글 남기기