Chapter 6

서비스와 네임스페이스

  • 6.1 서비스 — NodePort
  • 6.2 서비스 — ClusterIP
  • 6.3 서비스 — LoadBalancer
  • 6.4 네임스페이스

파드와 Deployment를 만들 수 있게 됐는데, 이것만으로는 트래픽을 받을 수가 없어. 서비스가 파드에 안정적인 접근점을 만들어주고, 네임스페이스가 클러스터를 논리적으로 분리해줘. 이 둘을 같이 다루는 이유는, 서비스의 DNS 이름이 네임스페이스에 따라 달라지기 때문이야.

Kubernetes 서비스는 애플리케이션 내부와 외부의 다양한 구성 요소 간 통신을 가능하게 해. 프론트엔드 파드 그룹, 백엔드 파드 그룹, 외부 데이터 소스에 연결하는 파드 그룹 등이 있을 때, 이들 간의 연결을 만들어주는 게 서비스야. 마이크로서비스 간의 느슨한 결합을 구현하는 핵심 요소지.

서비스 유형은 세 가지야. NodePort는 노드의 포트에서 내부 파드에 접근할 수 있게 해. ClusterIP는 클러스터 내부에 가상 IP를 만들어서 서로 다른 서비스 간 통신을 가능하게 해. LoadBalancer는 지원되는 클라우드 제공업체에서 로드 밸런서를 프로비저닝해.

먼저 NodePort부터 보자. 외부에서 파드에 접근하려면, 예를 들어 노트북(192.168.1.1)에서 Kubernetes 노드(192.168.1.2) 안의 파드(10.244.0.2)에 접근하려면, 파드의 내부 네트워크는 별도이기 때문에 직접 접근이 안 돼. 노드에 SSH로 접속해서 curl로 파드에 접근하는 건 가능하지만, 우리가 원하는 건 노트북에서 바로 접근하는 거잖아. 이때 NodePort 서비스가 노드의 포트와 파드의 포트를 매핑해줘.

세 가지 포트가 관련돼. targetPort(80)는 실제 웹 서버가 실행되는 파드의 포트야. port(80)는 서비스 자체의 포트야. 서비스는 클러스터 내부의 가상 서버 같아서 자체 IP(클러스터 IP)를 가져. nodePort(30008)는 노드 외부에서 접근하는 포트인데, 유효 범위가 30000~32767이야.

NodePort 서비스 정의 파일은 이래:

apiVersion: v1
kind: Service
metadata:
  name: myapp-service
spec:
  type: NodePort
  ports:
    - targetPort: 80
      port: 80
      nodePort: 30008
  selector:
    app: myapp

ports는 배열이니까 대시(-)로 시작해. 필수 필드는 port뿐이야. targetPort를 안 주면 port와 같다고 가정하고, nodePort를 안 주면 30000~32767 범위에서 자동 할당돼.

서비스를 파드에 연결하려면 selector를 써. 파드 정의 파일에서 레이블을 가져와서 selector 아래에 넣으면 돼.

kubectl create -f service-definition.yaml
kubectl get services
curl 192.168.1.2:30008

여러 파드가 있는 경우, 같은 레이블을 가진 파드가 여러 개 있으면 서비스가 자동으로 세 파드 모두를 엔드포인트로 선택해. 추가 구성 없이 무작위 알고리즘으로 부하를 분산시켜. 내장된 로드 밸런서 역할을 하는 거야.

파드가 여러 노드에 분산된 경우, 서비스를 만들면 Kubernetes가 자동으로 클러스터의 모든 노드에 걸쳐 서비스를 생성하고, 같은 nodePort를 모든 노드에 매핑해. 그래서 클러스터의 어떤 노드 IP로든 같은 포트로 접근할 수 있어.

단일 파드든, 단일 노드의 여러 파드든, 여러 노드의 여러 파드든, 서비스 생성 과정은 완전히 동일해. 추가 단계가 필요 없어. 파드가 추가되거나 제거되면 서비스가 자동으로 업데이트되니까 매우 유연해.

NodePort가 외부 접근용이라면, ClusterIP는 내부 통신용이야. 풀스택 웹 애플리케이션에는 보통 여러 종류의 파드가 있어. 프론트엔드 웹 서버 파드, 백엔드 서버 파드, Redis 같은 키-값 저장소 파드, MySQL 같은 데이터베이스 파드 등. 웹 프론트엔드는 백엔드와 통신해야 하고, 백엔드는 데이터베이스나 Redis와 통신해야 해.

파드마다 IP 주소가 할당되어 있지만, 이 IP는 고정이 아니야. 파드는 언제든 죽을 수 있고 새로 만들어지니까. 그래서 파드 IP를 내부 통신에 직접 쓸 수 없어. 게다가 프론트엔드 파드가 백엔드에 연결하려면 백엔드 파드 3개 중 어디로 가야 하는지, 누가 그 결정을 내리는지도 문제야.

ClusterIP 서비스가 이 문제를 해결해. 파드들을 그룹화하고, 그 그룹에 접근할 수 있는 단일 인터페이스(IP + 이름)를 제공하는 거야. 예를 들어 백엔드 파드용 서비스를 만들면, 모든 백엔드 파드를 하나로 묶고 다른 파드가 이 서비스를 통해 접근할 수 있게 해줘. 요청은 서비스 아래 파드 중 하나에 무작위로 전달돼.

이렇게 하면 각 계층을 독립적으로 확장하거나 이동할 수 있고, 서비스 간 통신에는 영향이 없어. 각 서비스에는 클러스터 내에서 IP와 이름이 할당되고, 다른 파드에서 그 이름으로 접근하면 돼.

ClusterIP 서비스 정의 파일은 이래:

apiVersion: v1
kind: Service
metadata:
  name: backend
spec:
  type: ClusterIP
  ports:
    - targetPort: 80
      port: 80
  selector:
    app: myapp
    type: backend

type이 ClusterIP인데, 사실 ClusterIP가 기본 타입이라 지정하지 않아도 자동으로 ClusterIP가 돼. targetPort는 백엔드가 노출되는 포트이고, port는 서비스가 노출되는 포트야. selector에 파드 정의 파일의 레이블을 넣어서 연결하면 끝이야.

kubectl create -f service-definition.yaml
kubectl get services

다른 파드에서 클러스터 IP 또는 서비스 이름을 사용해서 이 서비스에 접근할 수 있어.

NodePort 서비스로 외부에서 애플리케이션에 접근할 수 있게 만들 수 있다고 했잖아. 근데 사용자에게 http://192.168.1.2:30008 같은 URL을 주는 건 좀 아니야. 사용자는 http://example-vote-app.com 같은 단일 URL을 원하거든.

4개 노드 클러스터에 프론트엔드 애플리케이션이 있다고 해보자. NodePort 서비스를 만들면 파드가 실제로 배포된 노드가 2개뿐이어도, 클러스터의 모든 노드의 IP와 해당 포트로 접근할 수 있어. 근데 이 여러 IP와 포트 조합을 사용자에게 줄 수는 없잖아.

이걸 해결하는 한 가지 방법은 로드 밸런서 전용 VM을 만들어서 HAProxy나 nginx 같은 걸 설치하고, 트래픽을 노드들로 라우팅하도록 구성하는 거야. 근데 이걸 직접 세팅하고 유지 관리하는 건 꽤 지루한 작업이야.

Google Cloud, AWS, Azure 같은 클라우드 플랫폼을 쓰고 있다면, Kubernetes가 해당 클라우드의 네이티브 로드 밸런서와 통합할 수 있어. 서비스 유형을 NodePort 대신 LoadBalancer로 설정하기만 하면 돼:

apiVersion: v1
kind: Service
metadata:
  name: myapp-service
spec:
  type: LoadBalancer
  ports:
    - targetPort: 80
      port: 80
      nodePort: 30008
  selector:
    app: myapp

이게 지원되는 클라우드 플랫폼에서만 작동한다는 걸 기억해. VirtualBox 같은 지원되지 않는 환경에서 LoadBalancer 타입을 설정하면, NodePort로 설정한 것과 동일한 효과를 얻어. 노드의 포트에 서비스가 노출되기만 하고, 외부 로드 밸런서 구성은 되지 않아.

서비스까지 다뤘으니 이제 네임스페이스 얘기를 해보자. 네임스페이스는 하나의 Kubernetes 클러스터를 논리적으로 분리하는 방법이야. 비유하면 서로 다른 "집"과 같아. 같은 집 안에서는 이름만으로 서로를 부르지만, 다른 집 사람을 부를 때는 전체 이름(성 포함)을 써야 하잖아. 각 집에는 자체 규칙과 리소스가 있고.

Kubernetes에서도 마찬가지야. 지금까지 이 과정에서 만든 파드, 배포, 서비스 같은 오브젝트는 전부 default 네임스페이스 안에서 만든 거야. 클러스터를 처음 설정하면 Kubernetes가 자동으로 몇 가지 네임스페이스를 만들어.

kube-system 네임스페이스에는 네트워킹 솔루션, DNS 서비스 같은 내부 목적의 파드와 서비스가 들어가. 사용자가 실수로 이걸 삭제하거나 수정하지 못하도록 별도 네임스페이스로 격리해놓은 거야. kube-public 네임스페이스에는 모든 사용자가 사용할 수 있어야 하는 리소스가 들어가.

소규모 클러스터에서는 default 네임스페이스만 써도 돼. 하지만 엔터프라이즈나 프로덕션 환경에서는 네임스페이스를 활용하는 게 좋아. 예를 들어 개발(dev)과 프로덕션(prod) 환경을 같은 클러스터에서 운영하면서 리소스를 분리하고 싶을 때, 각각의 네임스페이스를 만들면 돼. 이러면 개발 작업 중에 프로덕션 리소스를 실수로 건드리는 일을 방지할 수 있어. 네임스페이스별로 누가 무엇을 할 수 있는지 정책을 설정할 수 있고, 리소스 할당량(Resource Quota)을 지정해서 각 네임스페이스가 사용할 수 있는 리소스양을 제한할 수도 있어.

같은 네임스페이스 안에서는 서비스 이름만으로 접근할 수 있어. 예를 들어 웹앱 파드가 같은 네임스페이스의 db-service에 접근하려면 그냥 db-service라고 하면 돼. 다른 네임스페이스의 서비스에 접근하려면 전체 DNS 이름을 써야 해:

db-service.dev.svc.cluster.local

이 형식은 서비스이름.네임스페이스.svc.cluster.local이야. 서비스가 생성될 때 이 형식으로 DNS 항목이 자동으로 추가되거든. cluster.local은 Kubernetes 클러스터의 기본 도메인 이름이고, svc는 서비스의 하위 도메인이야.

다른 네임스페이스의 파드를 나열하려면 --namespace 옵션을 써:

kubectl get pods --namespace=kube-system

파드를 특정 네임스페이스에 생성하려면:

kubectl create -f pod-definition.yaml --namespace=dev

아니면 파드 정의 파일의 metadata 섹션에 직접 네임스페이스를 지정할 수도 있어. 이렇게 하면 명령줄에서 네임스페이스를 안 줘도 항상 해당 네임스페이스에 생성돼.

새 네임스페이스를 만드는 방법은 두 가지야. 정의 파일을 쓰거나:

apiVersion: v1
kind: Namespace
metadata:
  name: dev

아니면 명령으로 바로:

kubectl create namespace dev

작업할 네임스페이스를 영구적으로 전환하려면:

kubectl config set-context --current --namespace=dev

이러면 매번 --namespace 옵션을 안 줘도 돼.

모든 네임스페이스의 파드를 한꺼번에 보려면:

kubectl get pods --all-namespaces

네임스페이스에서 리소스를 제한하려면 ResourceQuota를 만들어:

apiVersion: v1
kind: ResourceQuota
metadata:
  name: compute-quota
  namespace: dev
spec:
  hard:
    pods: "10"
    requests.cpu: "4"
    requests.memory: 5Gi
    limits.cpu: "10"
    limits.memory: 10Gi

정리

6장 읽고 기억할 거 세 가지:

  1. 서비스 3종: NodePort(외부), ClusterIP(내부), LoadBalancer(클라우드). selector로 파드를 찾고, 여러 파드가 있으면 자동 부하 분산. ClusterIP가 기본값
  2. 서비스 DNS는 네임스페이스를 따라. 같은 네임스페이스면 서비스 이름만으로, 다른 네임스페이스면 서비스.네임스페이스.svc.cluster.local 전체 경로로 접근
  3. 네임스페이스 = 논리적 격리. 정책과 ResourceQuota로 누가 뭘 할 수 있는지, 얼마나 쓸 수 있는지 제한할 수 있어