Kubernetes의 Ingress: 애플리케이션을 외부로 노출하기

1. 개요

cluster에서 실행 중인 Service가 있지만, 외부 사용자가 지저분한 IP:port 대신 api.nonladev.com 같은 멋진 도메인으로 접근하기를 원한다면, Ingress가 필요합니다. 이 글은 Ingress가 무엇인지, Service type LoadBalancer와의 차이점, Ingress Controller 설치 방법, 그리고 일반적인 상황에 대한 Ingress resource 작성 방법을 설명합니다: path 기반 라우팅, host 기반 라우팅, TLS. 이 글을 끝까지 읽으면 도메인과 HTTPS로 Spring Boot 애플리케이션을 외부에 노출할 수 있게 됩니다.

2. Ingress가 필요한 이유

2.1. Service의 문제점

Service type NodePort 또는 LoadBalancer는 잘 작동하지만 제한사항이 있습니다:

  • LoadBalancer: 각 Service마다 별도의 external IP가 필요 → 많은 service가 있으면 비용이 들어갑니다 (각 cloud LB는 별도로 요금이 부과됩니다).
  • NodePort: 이상한 포트를 노출합니다 (30000-32767), TLS termination이 없고, domain/path에 따른 라우팅이 없습니다.

외부에 노출해야 하는 5개의 microservice가 있다고 가정하면, LoadBalancer를 사용한다는 것은 5개의 cloud load balancer, 5개의 external IP를 의미합니다. Ingress는 하나의 entry point를 사용하여 hostname이나 path에 기반해 내부의 많은 Service로 traffic을 라우팅함으로써 이를 해결합니다.

2.2. Ingress란 무엇인가

Ingress는 외부에서 cluster 내부의 Service로 들어오는 HTTP/HTTPS 라우팅 규칙을 정의하는 API object입니다. Ingress 자체는 아무것도 하지 않습니다 — Ingress Controller (NGINX, Traefik, HAProxy 같은 reverse proxy를 실행하는 Pod)가 필요하여 규칙을 읽고 실행합니다.

Client → Ingress Controller (LoadBalancer/NodePort) → Ingress rules → Service → Pod

주의: 만약 cluster에 Ingress Controller를 설치하지 않았다면, Ingress resource를 만들어도 아무 효과가 없습니다 — controller가 읽을 때까지 “기다리는” 설정일 뿐입니다.

3. Ingress Controller 설치

가장 널리 사용되는 것은 ingress-nginx입니다. Helm으로 설치합니다:

<!-- 해당 사항 없음 — Helm/kubectl 사용, Maven dependency 아님 -->
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
helm install ingress-nginx ingress-nginx/ingress-nginx \
  --namespace ingress-nginx --create-namespace

이 명령은 ingress-nginx namespace를 생성하고, Ingress Controller를 배포합니다. 외부의 traffic을 받기 위한 type LoadBalancer인 Service도 함께 생성됩니다. 확인해봅시다:

kubectl get pods -n ingress-nginx
kubectl get svc -n ingress-nginx
NAME                                       TYPE           EXTERNAL-IP     PORT(S)
ingress-nginx-controller                   LoadBalancer   34.123.45.67    80:31234/TCP,443:31235/TCP

이 external IP가 cluster의 모든 Ingress에 대한 유일한 진입점입니다.

4. 예제 Deployment과 Service

Ingress를 작성하기 전에, 라우팅할 대상이 되는 Service가 필요합니다. order-service 애플리케이션이 있다고 가정합시다:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
spec:
  replicas: 2
  selector:
    matchLabels:
      app: order-service
  template:
    metadata:
      labels:
        app: order-service
    spec:
      containers:
        - name: order-service
          image: nonladev/order-service:1.0
          ports:
            - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: order-service
spec:
  selector:
    app: order-service
  ports:
    - port: 80
      targetPort: 8080

order-service Service는 port 80을 노출하고, container port 8080으로 forward합니다. Service는 type LoadBalancer를 사용할 필요가 없다는 점에 주목하세요. Ingress Controller가 내부 네트워크를 통해 Service로 접근할 것이므로 ClusterIP (기본값)로 충분합니다.

5. 기본 Ingress resource — host 기반 라우팅

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: order-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  rules:
    - host: api.nonladev.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: order-service
                port:
                  number: 80

각 부분을 설명하겠습니다:

  • ingressClassName: nginx — 어느 controller가 이 규칙을 처리할 것인지 지정합니다 (cluster에 여러 controller가 있을 때 중요합니다).
  • rules.host — Host: api.nonladev.com 헤더를 가진 traffic만 이 규칙과 일치합니다.
  • path + pathType: Prefix — /로 시작하는 모든 request (즉, 전체)가 order-service:80으로 forward됩니다.
  • annotation rewrite-target — ingress-nginx에만 해당되며, forward하기 전에 URL을 다시 쓰는 데 사용됩니다 (controller마다 annotation이 다릅니다).

적용하고 테스트해봅시다:

kubectl apply -f order-ingress.yaml
curl -H "Host: api.nonladev.com" http://34.123.45.67/
{"service":"order-service","status":"UP"}

실제 환경에서는 DNS record api.nonladev.com을 Ingress Controller의 external IP로 지정하므로, 위처럼 가짜 Host 헤더를 사용할 필요가 없습니다.

6. Path 기반 라우팅 — 여러 service가 하나의 domain 공유

일반적인 상황: 하나의 domain, 다양한 path에 따른 여러 service.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-gateway-ingress
spec:
  ingressClassName: nginx
  rules:
    - host: api.nonladev.com
      http:
        paths:
          - path: /orders
            pathType: Prefix
            backend:
              service:
                name: order-service
                port:
                  number: 80
          - path: /customers
            pathType: Prefix
            backend:
              service:
                name: customer-service
                port:
                  number: 80

api.nonladev.com/orders/123으로 오는 request는 order-service로 라우팅되고, /customers/55는 customer-service로 라우팅됩니다. pathType: Prefix는 prefix에 따른 매칭을 의미합니다 — Exact (정확히 일치)와 ImplementationSpecific (controller 자체가 해석, 보통 regex용)와 다릅니다.

주의: Ingress v1에서는 path의 순서가 매칭에 영향을 주지 않습니다 — controller는 자동으로 가장 긴 path를 먼저 매칭합니다. 하지만 path를 명확하게 작성하고 겹치지 않도록 하면 디버깅이 어려운 버그를 피할 수 있습니다.

7. TLS — Ingress를 위한 HTTPS

HTTPS를 사용하려면 cert과 key를 포함한 Secret이 필요하고, Ingress에서 이를 참조해야 합니다.

kubectl create secret tls nonladev-tls \
  --cert=tls.crt --key=tls.key
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: order-ingress-tls
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - api.nonladev.com
      secretName: nonladev-tls
  rules:
    - host: api.nonladev.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: order-service
                port:
                  number: 80

spec.tls은 어느 host가 어느 cert를 사용할지 선언합니다. Controller는 자동으로 TLS termination을 수행합니다 — 즉, Ingress에서 HTTPS를 해독하고, 내부의 Service로 순수 HTTP를 forward합니다. Pod는 TLS에 대해 알 필요가 없습니다.

실제로는 cert-manager를 사용하여 Let’s Encrypt cert를 자동으로 요청합니다:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: order-ingress-tls
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - api.nonladev.com
      secretName: nonladev-tls
  rules:
    - host: api.nonladev.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: order-service
                port:
                  number: 80

annotation cluster-issuer는 cert-manager에 자동으로 Secret nonladev-tls을 생성하고 갱신하도록 요청합니다 — 추가로 손으로 할 일이 없습니다.

8. 노출된 endpoint를 확인하는 테스트 작성

Ingress가 작동한 후, controller class가 path를 올바르게 처리하는지 확인하는 간단한 integration test를 작성할 수 있습니다 (YAML을 직접 테스트하는 것이 아닌 Spring Boot 애플리케이션 레벨 테스트):

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class OrderControllerIT {

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void shouldReturnOrderWhenCalledWithOrdersPrefix() {
        ResponseEntity<String> response =
                restTemplate.getForEntity("/orders/123", String.class);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).contains("orderId");
    }
}

이 테스트는 Spring Boot 애플리케이션이 Ingress가 rewrite하여 forward하는 /orders/123 path에 올바르게 응답하는지 확인합니다 — backend 코드 오류와 Ingress 설정 오류를 분리합니다.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

9. 일반적인 실수

  • ingressClassName 빠뜨림: cluster에 여러 Ingress Controller가 있으면 (예: nginx와 traefik 모두), Ingress가 어느 controller를 사용할지 불명확 → 작동하지 않거나 둘 다 잘못 처리합니다.
  • Service type 잘못 설정: backend Service를 LoadBalancer로 설정하는 것은 필요 없고 낭비입니다 — ClusterIP만으로 충분합니다.
  • pathType이 기대값과 일치하지 않음: Exact가 필요한데 Prefix를 사용하면 sub-request (/orders/123)가 404가 됩니다. 정확한 /orders만 일치하기 때문입니다.
  • DNS가 아직 설정되지 않음: 실제 DNS 설정 전에 curl -H "Host: ..." 명령으로 테스트하여 DNS propagation 대기 시간을 절약합니다.
  • Rewrite annotation이 controller와 맞지 않음: ingress-nginx의 annotation은 Traefik이나 AWS ALB Ingress Controller에는 적용되지 않습니다 — 각 controller는 고유한 annotation 세트를 가지므로, 해당 문서를 꼼꼼히 읽어야 합니다.

Ingress를 사용하지 말아야 할 때: 애플리케이션이 HTTP/HTTPS가 아닌 protocol을 사용한다면 (예: 원본 TCP, 복잡한 gRPC streaming, 또는 UDP), 표준 Ingress는 잘 지원하지 않습니다 — Gateway API 또는 LoadBalancer type Service를 직접 사용하여 TCP/UDP를 처리하는 것을 고려해야 합니다.

10. 결론

Ingress는 집중식 HTTP/HTTPS 라우팅 레이어로, 하나의 entry point를 통해 많은 Service를 외부로 노출하고, LoadBalancer 비용을 절감하며, domain/path/TLS를 쉽게 관리할 수 있게 해줍니다. 기억해야 할 핵심 사항: Ingress resource는 단지 규칙일 뿐이고, 실행하려면 Ingress Controller가 필요합니다; 올바른 ingressClassName과 pathType을 선택하세요; TLS를 자동화하려면 cert-manager를 사용하세요. HTTP가 아닌 애플리케이션의 경우, Ingress를 억지로 사용하지 말고 Gateway API를 고려해봅시다.

kubernetesingressnetworkingdevopsnginx