소개
Service 는 클러스터 내부에서 안정적인 애플리케이션 엔드포인트를 제공합니다. Ingress 는 Service 앞에 HTTP 라우팅 규칙을 추가하여 여러 애플리케이션이 호스트 이름이나 URL 경로를 기준으로 하나의 진입점을 공유할 수 있게 합니다.
이 실습에서는 미리 준비된 ingress-nginx 컨트롤러를 확인하고, 두 개의 백엔드를 배포한 다음 호스트 및 경로 기반 라우팅을 추가합니다. 또한 Ingress, Service, Pod 를 거쳐 요청이 전달되는 과정을 추적합니다.
실습 내내 요청 경로를 기억해 두세요. 클라이언트 → ingress-nginx 진입점 → Ingress 규칙 → Service → 준비된 Pod 의 순서입니다. 요청이 실패했을 때 Ingress 를 하나의 블랙박스로 취급하기보다 이 연결 고리를 순서대로 확인하는 편이 훨씬 쉽습니다.
Ingress 컨트롤러 확인
이 단계에서는 CKA 환경에서 이미 제공된 컨트롤러를 확인하며, 별도의 컨트롤러를 설치하지 않습니다.
Ingress 객체는 설정일 뿐 프록시 프로세스가 아닙니다. 호환되는 컨트롤러가 이를 감시하지 않으면 아무런 효과가 없습니다. 먼저 두 가지 인프라 구성 요소가 모두 존재하는지 확인합니다. 바로 nginx라는 이름의 IngressClass와 사용 가능한 ingress-nginx 컨트롤러 Deployment 입니다.
cd /home/labex/project/ingress-routing
kubectl get ingressclasses
kubectl get deployments,pods,services -n ingress-nginx
kubectl wait --for=condition=Available deployment/ingress-nginx-controller -n ingress-nginx --timeout=60s
Ingress 리소스에는 라우팅 의도가 저장됩니다. ingress-nginx 컨트롤러는 ingressClassName이 nginx인 리소스를 감시하고 실제 프록시를 구성합니다.
컨트롤러 Service 는 이 로컬 클러스터에서 NodePort 를 통해 프록시를 외부에 노출합니다. 해당 포트는 값을 직접 지정하지 않고 나중에 조회합니다.
준비된 환경에서는 nginx 클래스, 사용 가능한 컨트롤러 하나, 그리고 다음과 같은 NodePort Service 가 표시됩니다.
NAME CONTROLLER
nginx k8s.io/ingress-nginx
deployment.apps/ingress-nginx-controller 1/1 ...
service/ingress-nginx-controller NodePort ... 80:.../TCP,443:.../TCP
함께 표시될 수 있는 Completed 상태의 admission Pod 두 개는 이미 완료된 설치 Job 입니다. 실패한 컨트롤러 복제본이 아닙니다.
두 개의 Service 백엔드 배포
이 단계에서는 하나의 여러 문서 파일에서 단일 복제본 웹 워크로드 두 개와 이에 대응하는 ClusterIP Service 를 생성합니다.
이 백엔드들은 Kubernetes 입문 과정에서 다룬 개념을 사용하므로, 여기서의 핵심은 Deployment 문법이 아니라 두 백엔드와 라우팅의 관계를 이해하는 것입니다. 파일에는 ---로 구분된 YAML 문서 다섯 개가 들어 있습니다. 먼저 Namespace 하나가 나오고, 각 애플리케이션에 대해 Deployment 와 Service 가 하나씩 이어집니다. 각 Service.spec.selector를 해당 Pod 의 레이블과 나란히 확인하세요. Ingress 가 요청을 전달하려면 두 값이 일치해야 합니다.
cd /home/labex/project/ingress-routing
cat <<'EOF' > backends.yaml
apiVersion: v1
kind: Namespace
metadata: {name: practice-ingress}
---
apiVersion: apps/v1
kind: Deployment
metadata: {name: storefront, namespace: practice-ingress}
spec:
replicas: 1
selector: {matchLabels: {app: storefront}}
template:
metadata: {labels: {app: storefront}}
spec:
containers: [{name: web, image: nginx:1.27-alpine, imagePullPolicy: IfNotPresent, ports: [{name: http, containerPort: 80}]}]
---
apiVersion: v1
kind: Service
metadata: {name: storefront, namespace: practice-ingress}
spec: {selector: {app: storefront}, ports: [{name: http, port: 80, targetPort: http}]}
---
apiVersion: apps/v1
kind: Deployment
metadata: {name: reports, namespace: practice-ingress}
spec:
replicas: 1
selector: {matchLabels: {app: reports}}
template:
metadata: {labels: {app: reports}}
spec:
containers: [{name: web, image: httpd:2.4-alpine, imagePullPolicy: IfNotPresent, ports: [{name: http, containerPort: 80}]}]
---
apiVersion: v1
kind: Service
metadata: {name: reports, namespace: practice-ingress}
spec: {selector: {app: reports}, ports: [{name: http, port: 80, targetPort: http}]}
EOF
다섯 개 문서를 한 번에 적용한 다음 두 Deployment 가 준비될 때까지 기다립니다. 각각을 따로 기다리면 이미지나 selector 가 잘못되었을 때 어느 백엔드에서 문제가 발생했는지 명확하게 알 수 있습니다.
kubectl apply -f backends.yaml
kubectl rollout status deployment/storefront -n practice-ingress --timeout=60s
kubectl rollout status deployment/reports -n practice-ingress --timeout=60s
Ingress 는 Service 로만 트래픽을 전달하므로 각 백엔드에 준비된 엔드포인트가 이미 있어야 합니다.
라우팅 규칙을 만들기 전에 다음 명령으로 이 전제 조건을 명시적으로 확인합니다.
kubectl get services,endpointslices -n practice-ingress
각 Service 에는 준비된 주소 하나가 포함된 대응 EndpointSlice 가 있어야 합니다.
service/reports ClusterIP ... 80/TCP
service/storefront ClusterIP ... 80/TCP
endpointslice.../reports-... IPv4 80 10.244.104...
endpointslice.../storefront-... IPv4 80 10.244.104...
생성되는 접미사와 주소는 환경에 따라 달라집니다. 하지만 각 Service 이름에 대해 비어 있지 않은 EndpointSlice 가 하나씩 있어야 합니다. 백엔드가 없는 Service 를 Ingress 가 복구해 줄 수는 없습니다. 따라서 이 확인 단계에서는 먼저 하위 계층이 정상인지 검증합니다.
호스트 기반 라우팅 추가
이 단계에서는 store.practice.test 요청을 storefront Service 로 라우팅합니다.
Ingress 규칙은 host, path, 백엔드 Service 포트라는 세 가지 기준으로 라우팅을 결정합니다. ingressClassName: nginx는 사용할 컨트롤러를 지정합니다. rewrite 어노테이션은 컨트롤러별 기능으로, 백엔드로 전달되는 요청 경로를 /로 변경합니다. 두 데모 웹 서버 모두 / 경로에 응답할 수 있습니다.
cd /home/labex/project/ingress-routing
cat <<'EOF' > ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: practice-routing
namespace: practice-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- host: store.practice.test
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: storefront
port: {name: http}
EOF
규칙을 적용한 뒤 describe 명령으로 컨트롤러가 해석한 내용을 확인합니다. 여기에는 실제로 연결된 백엔드 엔드포인트도 포함됩니다.
kubectl apply -f ingress.yaml
kubectl describe ingress practice-routing -n practice-ingress
pathType: Prefix는 / 및 그 하위 경로와 일치합니다. rewrite 어노테이션은 선택된 요청을 백엔드의 / 경로로 전달하며, 이곳에서 데모 웹 서버가 페이지를 제공합니다. 호스트는 HTTP 라우팅 키일 뿐이므로 이 로컬 테스트에 공용 DNS 가 필요하지 않습니다.
describe 출력에서는 규칙이 Pod 주소까지 연결되어 표시되어야 합니다.
Rules:
Host Path Backends
store.practice.test
/ storefront:http (10.244.104...:80)
생성 직후 최상위 Address가 비어 있어도 이 로컬 환경에서는 실패를 의미하지 않습니다. 중요한 것은 storefront:http 백엔드와 해당 Pod 주소가 확인되는지 여부입니다.
경로 라우팅 추가 및 요청 테스트
이 단계에서는 /reports 라우팅을 추가하고 노드 진입점을 통해 두 규칙을 모두 테스트합니다.
JSON Patch 경로의 마지막에 있는 /-는 배열에서 새 항목을 기존 항목 대신 추가하라는 뜻입니다. 따라서 기존 / 경로를 바꾸지 않고 새 경로를 추가합니다. 새 백엔드는 이름이 http인 포트를 통해 reports Service 로 연결됩니다.
다음 추가 패치는 한 번만 실행하세요. kubectl apply와 달리 /-에 항목을 추가하는 작업은 멱등적이지 않습니다. 같은 명령을 반복하면 이전 항목을 인식하지 않고 동일한 /reports 항목을 또 추가합니다.
다음 두 변수 할당은 엔드포인트를 직접 지정하지 않고 환경에서 조회합니다. minikube ip -p labex-v135는 지정된 Minikube 프로필의 노드 IP 를 반환하고, 명령 치환 $(...)는 각 명령의 출력을 셸 변수에 저장합니다. JSONPath 쿼리는 http라는 이름의 Service 포트를 선택하고 해당 NodePort 를 추출합니다.
kubectl patch ingress practice-routing -n practice-ingress --type='json' -p='[{"op":"add","path":"/spec/rules/0/http/paths/-","value":{"path":"/reports","pathType":"Prefix","backend":{"service":{"name":"reports","port":{"name":"http"}}}}}]'
NODE_IP=$(minikube ip -p labex-v135)
HTTP_NODE_PORT=$(kubectl get service ingress-nginx-controller -n ingress-nginx -o jsonpath='{.spec.ports[?(@.name=="http")].nodePort}')
echo "Ingress endpoint: ${NODE_IP}:${HTTP_NODE_PORT}"
두 변수는 환경마다 달라지는 주소를 안내문에 직접 삽입하지 않도록 해 줍니다. 이제 규칙에 필요한 호스트 헤더를 포함해 요청을 보냅니다. 각 curl 명령에서 -s는 진행률 표시를 숨기고, --retry 5는 일시적인 실패가 발생하면 재시도합니다. --retry-connrefused는 처음에 연결이 거부된 경우에도 재시도하게 하며, --retry-delay 2는 재시도 사이에 2 초간 기다립니다. -H는 HTTP Host 헤더를 설정하고, 파이프는 응답을 grep으로 전달하여 식별에 필요한 페이지 텍스트만 표시합니다.
curl -s --retry 5 --retry-connrefused --retry-delay 2 -H 'Host: store.practice.test' "http://${NODE_IP}:${HTTP_NODE_PORT}/" | grep 'Welcome to nginx'
curl -s --retry 5 --retry-connrefused --retry-delay 2 -H 'Host: store.practice.test' "http://${NODE_IP}:${HTTP_NODE_PORT}/reports" | grep 'It works'
두 응답을 통해 하나의 진입점이 서로 다른 Service 로 요청을 라우팅한다는 사실을 확인할 수 있습니다.
첫 번째 응답은 NGINX 에서, 두 번째 응답은 Apache httpd 에서 반환됩니다. 서로 다른 페이지 텍스트가 나타나는 것은 프록시가 단순히 HTTP 200 을 반환한 것이 아니라 경로 규칙에 따라 다른 백엔드를 선택했다는 확실한 증거입니다.
일치해야 하는 출력은 다음과 같습니다.
<title>Welcome to nginx!</title>
<h1>Welcome to nginx!</h1>
<title>It works! Apache httpd</title>
<p>It works!</p>
이 결과는 사용자 지정 Host 헤더가 중요한 이유도 보여 줍니다. 두 요청은 동일한 노드 IP 와 NodePort 에 도달하지만, 실제로 어떤 규칙과 백엔드를 선택할지는 호스트와 경로가 결정합니다.
요약
미리 준비된 ingress-nginx 컨트롤러를 재사용하고, Ingress 규칙을 준비된 Service 에 연결한 다음, 하나의 진입점을 통해 호스트 및 경로 기반 라우팅을 테스트했습니다.


