Skip to content

port와 targetPort만 적으면 외부에서 안 열리나 (docker -p와 무엇이 다른가) #28

Description

@sysnet4admin

4회차에 이런 질문이 나왔습니다. 세 번에 걸쳐 물어봐 주셨는데 마지막에 스스로 정리해 주신 문장이 핵심입니다.

Service의 port와 targetPort가 따로 존재하는 이유가 궁금합니다. (docker -p 명령어와 매칭 시켜서 이해하려니 헷갈리네요)

밖의 포트는 nodePort라는게 또 있는것 같아서 헷갈리네요...

결국 요약하자면 제 질문은 docker run -p 외부노출포트:컨테이너내부포트 에선 외부노출포트가 바로 ip로 접근가능하게 뚫리지만, k8s에서는 port와 targetPort의 기입만으로 뚫리는게 아니지 않나? 였습니다.

답부터

Service 타입에 따라 다릅니다. 그리고 지금 여러분이 쓰고 계신 타입에서는 porttargetPort만으로 외부에서 열립니다.

k8s/frontend-service.yaml을 보시면 이렇습니다.

spec:
  type: LoadBalancer
  ports:
    - port: 80
      targetPort: 80

nodePort가 적혀 있지 않은데도 브라우저에서 그 IP로 그냥 접속됩니다. 지금까지 그렇게 화면을 보셨습니다. 30000번대를 친 적이 없습니다.

AI에게 물어보신 답은 type: NodePort인 경우를 말한 것입니다. 그 경우에는 맞는 말입니다. 다만 타입이 세 가지이고 답이 타입마다 다릅니다.

포트가 세 개인 이유

각각 다른 문입니다. 어디에 있는 문인지가 다릅니다.

필드 어디에 있는 문인가 누가 두드리나
targetPort Pod의 컨테이너가 열어 둔 문 Service
port Service 자신의 문 같은 클러스터 안의 다른 Pod, 그리고 로드밸런서
nodePort 노드(서버) 자체의 문 클러스터 밖에서 노드 IP로 직접
flowchart LR
  B["브라우저<br/>(클러스터 밖)"]
  OP["같은 클러스터의<br/>다른 Pod"]
  LB["클라우드 로드밸런서<br/>외부 IP : port"]
  NODE["노드 IP : nodePort<br/>30000-32767"]
  SVC["Service<br/>ClusterIP : port"]
  POD["Pod<br/>: targetPort"]

  B -->|"LoadBalancer일 때"| LB
  B -->|"NodePort일 때"| NODE
  OP -->|"항상 가능"| SVC
  LB --> SVC
  NODE --> SVC
  SVC --> POD

  class POD target
  classDef target fill:#166534,stroke:#4ade80,color:#ffffff
Loading

port와 targetPort가 왜 따로인가

부르는 쪽이 Pod 사정을 몰라도 되게 하려는 것입니다.

앱이 3001번에서 돌고 있어도 부르는 쪽은 80번으로 부릅니다. 나중에 앱이 포트를 8000번으로 바꿔도 부르는 쪽 코드는 그대로입니다. Service의 targetPort만 고치면 됩니다.

문서의 서술입니다.

A Service can map any incoming port to a targetPort. By default and for convenience, the targetPort is set to the same value as the port field.
(Service는 들어오는 port를 아무 targetPort로나 연결할 수 있습니다. 기본값은 편의상 port와 같은 값입니다.)

우리 frontend-service는 둘 다 80이라 차이가 안 보입니다. 기본값이 같은 값이기 때문입니다. 그래서 왜 두 개인지 이해가 안 되는 것이 자연스럽습니다.

실제로 다른 예를 보시면 바로 잡힙니다. 이건 제 실습용 클러스터의 Service 목록입니다.

NAME         TYPE           CLUSTER-IP       PORT   TARGET   NODEPORT
kubernetes   ClusterIP      10.96.0.1        443    6443     <none>
mcp-a        LoadBalancer   10.105.3.14      80     3001     30128
mcp-b        LoadBalancer   10.106.17.130    80     8000     30840

mcp-a를 보시면 숫자 세 개가 전부 다릅니다. 밖에서는 80으로 부르고, 노드 IP로 오면 30128이고, 실제 앱은 3001에서 돌고 있습니다. 부르는 쪽은 3001을 몰라도 됩니다.

kubernetes Service도 443에서 6443으로 바뀝니다. 이건 여러분 클러스터에도 있습니다.

한 걸음 더. targetPort에는 숫자 대신 이름을 쓸 수도 있습니다.

ports:
  - port: 80
    targetPort: http-web-svc

Pod가 자기 포트에 http-web-svc라고 이름을 붙여 두면 Service는 그 이름만 알면 됩니다. 번호가 바뀌어도 Service는 안 고쳐도 됩니다. 두 필드를 나눈 목적이 여기서 분명해집니다.

docker -p와 무엇이 다른가

가장 헷갈리시는 부분이라 표로 놓겠습니다.

Docker 쿠버네티스
쓰는 법 docker run -p 8080:3001 Service의 porttargetPort
왼쪽 숫자가 여는 문 내 컴퓨터(호스트)의 문 Service의 문 (클러스터 안의 가상 주소)
오른쪽 숫자 컨테이너가 여는 문 Pod가 여는 문
대상이 몇 개인가 컨테이너 하나 라벨에 맞는 Pod 전부
밖에서 바로 되나 됩니다. 내 컴퓨터 IP로 타입에 따라 다릅니다

그림으로 놓으면 차이가 분명해집니다. 먼저 Docker입니다.

flowchart LR
  DB["브라우저"] -->|"내 컴퓨터 IP : 8080"| DH["호스트의 8080<br/>(-p 왼쪽)"]
  DH -->|"연결"| DC["컨테이너의 3001<br/>(-p 오른쪽)"]

  class DH host
  classDef host fill:#334155,stroke:#94a3b8,color:#ffffff
Loading

기계 한 대 안에서 층이 둘입니다. 호스트의 문과 컨테이너의 문이고, 끝은 컨테이너 하나입니다.

이미지 하나를 빌드해 놓고, 그것을 로컬에서 부를 때와 클라우드에서 부를 때를 나란히 놓으면 이렇습니다. 이 과정에서 실제로 하는 일이라 값을 그대로 썼습니다.

flowchart TB
  B["app/frontend 코드를 docker build<br/>이미지 ssf15-frontend:v1 (컨테이너 안은 80)"]

  subgraph L["이 이미지를 내 컴퓨터에서 부를 때"]
    direction LR
    L1["브라우저"] -->|"localhost : 8080"| L2["호스트 8080<br/>docker run -p 8080:80"] --> L3["컨테이너 : 80"]
  end

  subgraph C["같은 이미지를 클라우드에서 부를 때"]
    direction LR
    C1["브라우저"] -->|"외부 IP : 80"| C2["로드밸런서"] --> C3["Service<br/>ClusterIP : 80"] --> C4["Pod : 80"]
  end

  B --- L
  L --- C

  class B same
  class L3 inside
  class C4 inside
  classDef same fill:#166534,stroke:#4ade80,color:#ffffff
  classDef inside fill:#166534,stroke:#4ade80,color:#ffffff
Loading

초록 칸을 보시기 바랍니다. 맨 위의 이미지 하나가 아래 두 줄의 끝에 그대로 놓여 있습니다. 컨테이너 안은 양쪽이 똑같이 80입니다. 내 컴퓨터에서 돌리든 클라우드에 올리든 이미지도 그 안의 포트도 바뀌지 않습니다.

두 줄의 차이는 가운데뿐입니다. 로컬은 -p가 문 하나를 뚫어서 바로 컨테이너로 갑니다. 클라우드는 로드밸런서와 Service가 차례로 놓입니다. 그래서 적을 포트가 늘어난 것이지 컨테이너가 복잡해진 것이 아닙니다.

6회차에 본인 이미지를 빌드해서 올리면 아래 줄을 직접 하시게 됩니다.

여기까지가 답입니다. 아래는 그 클라우드 쪽 줄을 좀 더 자세히 본 것입니다.

flowchart LR
  KB["브라우저"] -->|"외부 IP : 80"| KL["로드밸런서<br/>클라우드가 만든<br/>진짜 주소"]
  KL --> KS["Service<br/>ClusterIP : 80<br/>(port)"]
  KS -->|"targetPort 3001"| KP1["Pod : 3001"]
  KS --> KP2["Pod : 3001"]
  KS --> KP3["Pod : 3001"]

  class KS mid
  classDef mid fill:#854d0e,stroke:#fbbf24,color:#ffffff
Loading

기계 여러 대에 걸쳐 층이 셋입니다. 노란 칸이 Docker에는 없는 층입니다. 그리고 끝이 Pod 여러 개로 갈라집니다.

두 그림의 차이는 이 두 가지가 전부입니다. 가운데 층이 하나 더 있다는 것, 그리고 끝이 하나가 아니라 여럿이라는 것입니다.

층이 하나 더 있습니다. Docker는 기계 한 대 안에서 호스트와 컨테이너 두 층입니다. 쿠버네티스는 여러 대를 묶어 놓아서 그 사이에 "클러스터 안의 가상 주소"라는 층이 하나 더 있습니다. Pod는 죽고 살아나며 IP가 바뀌는데 그 층 덕분에 부르는 쪽 주소가 안 바뀝니다.

그리고 docker -p의 왼쪽 숫자는 그 기계의 진짜 포트입니다. Service의 port는 진짜 기계의 포트가 아니라 클러스터 안에서만 통하는 가상 주소의 포트입니다. 그래서 그것만으로는 밖에서 안 보이는 것이 맞습니다. 밖에서 들어오게 하는 것은 타입이 정합니다.

타입이 정하는 것

타입 밖에서 접근 무엇으로
ClusterIP 안 됩니다 클러스터 안에서만 ClusterIP:port
NodePort 됩니다 노드IP:nodePort. 기본 범위는 30000에서 32767
LoadBalancer 됩니다 외부IP:port. 클라우드가 만들어 준 주소

k8s/backend-service.yamlClusterIP이고 k8s/frontend-service.yamlLoadBalancer입니다. backend는 밖에서 못 부르고 frontend는 부를 수 있습니다. 4회차에 이 둘을 나눈 이유가 이것입니다.

LoadBalancer는 NodePort도 같이 만듭니다. 위 목록에서 mcp-aLoadBalancer인데 NODEPORT 열에 30128이 찍혀 있는 것이 그 증거입니다. 적은 적이 없는데 생겼습니다. 문서의 서술입니다.

By default, spec.allocateLoadBalancerNodePorts is true and type LoadBalancer Services will continue to allocate node ports.
(기본값이 true이며 LoadBalancer 타입 Service는 노드 포트를 계속 할당합니다.)

즉 LoadBalancer는 NodePort 위에 외부 로드밸런서를 하나 더 얹은 것입니다. 그래서 외부 IP로도 되고 노드 IP의 30128로도 됩니다. 다만 밖에서 쓰기 편한 쪽은 외부 IP이고 여러분은 그쪽을 쓰고 계십니다.

8080으로 열고 싶으면

질문하신 그 지점입니다. Ingress 없이 IP:8080으로 열고 싶을 때입니다.

LoadBalancer라면 됩니다. port를 8080으로 적으면 외부 IP의 8080으로 접속됩니다.

spec:
  type: LoadBalancer
  ports:
    - port: 8080
      targetPort: 80

NodePort라면 안 됩니다. 8080은 기본 범위 30000에서 32767 밖입니다. 범위를 바꾸려면 API 서버의 --service-node-port-range 설정을 고쳐야 하는데, GKE 같은 관리형 서비스에서는 컨트롤 플레인을 클라우드가 관리하므로 학생이 바꿀 수 없습니다.

그래서 정리하면 이렇습니다. Docker처럼 원하는 번호로 열고 싶으면 LoadBalancer를 쓰고 port를 그 번호로 적으면 됩니다. NodePort는 번호를 고를 자유가 좁습니다.

AI 답을 검증하는 사례이기도 합니다

받으신 답이 틀린 것은 아닙니다. NodePort 타입에 대해서는 정확합니다. 다만 질문에 타입이 안 적혀 있었고, AI가 한 가지 경우만 놓고 답했습니다.

#20에 적었듯이 AI에게 물을 때 조건을 같이 주면 답이 달라집니다. 이 경우라면 이렇게 물으면 됩니다.

type: LoadBalancer 인 Service에서 port: 8080, targetPort: 80으로 두면
외부에서 IP:8080으로 접근할 수 있어? NodePort를 따로 지정해야 해?

그리고 받은 답이 맞는지 확인할 방법이 이미 손에 있습니다. 지금 브라우저로 접속되는 그 주소가 반례입니다.

직접 확인해 보시기 바랍니다

# 세 필드를 한 번에 봅니다
kubectl get svc -o custom-columns='NAME:.metadata.name,TYPE:.spec.type,CLUSTER-IP:.spec.clusterIP,PORT:.spec.ports[*].port,TARGET:.spec.ports[*].targetPort,NODEPORT:.spec.ports[*].nodePort'

# frontend-service에 nodePort가 생겼는지
kubectl get svc frontend-service -o jsonpath='{.spec.ports[0]}'

# backend-service는 왜 EXTERNAL-IP가 없는지
kubectl get svc backend-service

frontend-service의 출력에 nodePort가 들어 있을 것입니다. 적은 적이 없는데 있습니다. #26에서 말한 "내가 쓴 적 없는 값"의 또 다른 예입니다.

이 글은 참고용입니다. 다만 세 필드가 각각 어느 문인지는 4회차 내용이라 한 번 보시기를 권합니다.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions