[문제기록] K8s+Ingress 로 Spring Boot 배포시 Tomcat 400 에러 관련

2024. 8. 12. 23:51·Logging/인프라
728x90
반응형

 

Ingress Controller 와 LB 를 중심으로 배포 구조를 설계하였다

 

 

사내 환경에서 Baremetal 용 Ingress Controller 를 설치해서 Spring Boot 애플리케이션을 이전하는 작업을 진행하였다. Ingress Controller 는 환경/목적 별로 정의서가 구분되어 있는 오픈 소스를 많이 활용한다. 사내는 외부와 연동이 어렵고 VM 을 할당받아 사용하기 때문에 Baremetal 용으로 사용하였다. 

(https://kubernetes.github.io/ingress-nginx/deploy/baremetal/)

 


Baremetal 용은 Ingress Controller 서비스가 Node Port 서비스이기 때문에 워커 노드들의 Node 를 활용해서 Ingress Controller로의  접근을 제어해야 한다. 따라서 모든 워커노드들에 대한 접근을 제어하기 위해 앞단에 Load Balance 용 Nginx 를 두었고, 다음과 같이 설정하였다 (그냥 단순 컨테이너로 하나 띄움)

 

 

upstream k8s-cluster{  # Node Port 로 각 Node 에 오픈된 포트로 LB 를 진행한다
    server {WORKER_NODE_1}:{INGRESS_OPEN_PORT};
    server {WORKER_NODE_2}:{INGRESS_OPEN_PORT};
}

server {
    listen 443 ssl;
    server_name   foo.samsungds.net;
    
    ssl_certificate  ~/~.crt; # SSL 해제는 LB 에서 진행한다
    ssl_certificate_key ~/~.key;
    
    location / {
        proxy_pass  http://k8s-cluster;           #  SSL Termination 진행
        proxy_set_header    HOST             $host;
        proxy_set_header    X-Real-IP        $remote_addr;
        proxy_set_header    X-Forwarded-For  $proxy_add_x_forwarded_for;
     }
}



X-Real-IP 는 이전에 Spring Application 에서 ServletRequest.getRemoteAddr() 을 통해 기존 요청지를 받기 위해 추가되었다. 이 때는 K8s 없이 앞단에 Nginx LB 만 두었었기 때문에 LB에서 직접 X-Real-IP 로 주소를 전달해 주었었다. 일단 Spring Boot 기준 X-Real-IP 로 들어오는건 tomcat 에 remoteip 설정을 해줘야 수신을 해주는건 맞는 것 같다.  그래서 별도의 tomcat 설정 없이 진행하기 위해서, X-Real-IP 는 이번 설정에서 제외하겠다. (어차피 X-Forwarded-For 를 해주면 IP들이 다 누적되어서  들어오게 된다) 

 

 

참고로  위 설정 중 proxy_set_header host 가 들어가지 않는다면, foo.something.net 이런 Host 값이 후속으로 전달되지 않는다. 따라서 Ingress 가 host 를 가지고 판단할 수가 없다! 따라서 HOST는 필수! (그리고 아래 더 설명되어 있는데, 이 때는 $host 로 넣어야 한다)



또한, Ingress 에서 Nginx Pod 에게 (Ingress 리소스 기동시 Ingress Controller 는 자신을 위한 Nginx Pod 을 만듬, Ingress 관련해서 어느정도 알고 있다는 전제하의 게시물) 전달할 Nginx 설정으로는 다음과 같이 정의하였다. (정의서 전체) 

 

 

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ing-ssomec
  namespace: production
  annotations:
    kubernetes.io/ingress.class: "nginx"
    #    kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/configuration-snippet: |
      proxy_set_header HOST $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
spec:  # SSL 해제는 LB 에서 진행
        #  tls:
        #  - hosts:
        #    - foo.net
        #    secretName: secret-ing-ssomec
  rules:
  - host: foo.net
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: svc-foo-be
            port:
              number: 80



헤더에 필요한 정보가 다 전달되어야 한다고 생각해서, Forward 를 많이 해주었고.. 사실 디버깅하다가 이것저것 넣어주다 더 많아졌다.. ㅋㅋㅋ 어쨌든 문제는 다음과 같은 Tomcat 에서 에러가 발생하는 점이였다. 

 

 

제일 무서운 no-hint 에러 페이지



그리고 이는 Spring Boot 이기 때문에, 내장된 Tomcat 에서 View 해주는 에러, 즉 Spring Layer 에 진입하기 전에 발생하는 에러로, Filter 단에서 발생하는 에러로 보였다. 다음과 같이 애플리케이션 yml 설정을 통해 Tomcat 의 로깅을 명확하게 추가해줬다. 

 

 

logging:
    level:
        org:
            springframework: DEBUG
            apache:
                catalina: DEBUG
                coyote: DEBUG
                tomcat:
                    util: DEBUG
            spring:
                web: DEBUG



DEBUG 로그로 톰캣이 수신한 요청에 본문들을 모두 확인할 수 있었다. 이 때, 정말 다양한 헤더들이 있고, Host 는 HOST 와 중복되어 있고 등등을 확인할 수 있었다. 아마 Ingress Nginx Pod 에서 default 로 전달해주도록 설정된 Header들과 와 중복이 되어서 발생하는 문제로 보였다 (이 한문장을 알아내는데 거진 이틀이 걸렸다). 그렇다면 Nginx 가 기본적으로 생성해주는 값들은 어떤 값들인지, 실제 Pod 에 들어가서 확인을 해봤다. (ingress nginx pod 으로 들어감) 복잡한 설정들 투성이나, 다음과 같은 설정 부분을 확인해줄 수 있다.

 

// ... nginx.conf 중
proxy_set_header Host                   $best_http_host;

# Pass the extracted client certificate to the backend
# Allow websocket connections
proxy_set_header                        Upgrade           $http_upgrade;
proxy_set_header                        Connection        $connection_upgrade;
proxy_set_header X-Request-ID           $req_id;
proxy_set_header X-Real-IP              $remote_addr;

proxy_set_header X-Forwarded-For        $remote_addr;

proxy_set_header X-Forwarded-Host       $best_http_host;
proxy_set_header X-Forwarded-Port       $pass_port;
proxy_set_header X-Forwarded-Proto      $pass_access_scheme;
proxy_set_header X-Forwarded-Scheme     $pass_access_scheme;

proxy_set_header X-Scheme               $pass_access_scheme;

# Pass the original X-Forwarded-For
proxy_set_header X-Original-Forwarded-For $http_x_forwarded_for;

# 버퍼 사이즈, timeout 시간, 쿠키 등 설정에 대한 부분 ###
 ... ~~~~~~~~
 
# Ingress Rule 에 직접 annotation 으로 추가한 부분은 맨 마지막에 추가된다
proxy_set_header HOST $host;
#      proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
#      proxy_set_header X-Forwarded-Proto $scheme;

// ...



 즉, X-Forwarded-For, Host, X-Real-IP, Proto 모두 추가를 기본적으로 해주기 때문에, 부가적으로 추가해줄 필요가 없던 것이다. 내가 해준 설정 중 Host 와 HOST 두개의 차이로 인해, Tomcat 이 Host IP 가 두 개 존재하므로, 비정상 요청이라고 판단한 부분이였던 것이다. 따라서 Ingress 에선 별도로 추가를 해주지 않아도, 잘 동작할 것이라고 예상하고 시도해봤으나, 작동하지 않았다(참고로 헤더 설정을 해주면 대체가 아니라 누적이 되더라... 즉 X-Forwarded-For 도 두번 해주면 두 번 들어간다). 어쨌든 작동하지 않은 이유를 자세히 살펴봤더니 다음과 같이 다른 부분이 있었다. 

 

 


1. proxy_set_header X-Forwarded-For  $remote_addr  VS  $proxy_add_x_forwarded_for

 


전자는 요청을 전달받은 Client IP 주소를 X-Forwarded-For 헤더의 값으로 설정하여, 기존 값을 덮어씌우게 된다 (X-Forwarded-For 은 하나의 Value 값만 존재). 하지만 후자는 기존 값에 요청을 전달받은 Client IP 주소를 추가한다. 즉, 기존 값이 있다면 그 값 뒤에 추가를 하게 된다

 

 


2. proxy_set_hader Host  $best_http_host  VS  $host

 

 

전자는 같은 설정 안에서 proxy_pass 로 설정한 URL 의 호스트 부분이 헤더로 설정되고, 후자는 Client 의 요청 URL 에서 호스트 부분을 헤더로 설정한다

 


아무튼 1번에서 X-Forwarded-For 에 proxy_add_x_forwarded_for (내가 했던 설정) 을 지워주게 되었으므로, 기본 설정인 $remote_addr 로 들어가서, Ingress Controller 가 인지한 Remote Address  만 헤더 값으로 들어가게 되고 App 단에서는 에러가 발생하였다 (Client 의 IP 를 사용하는 서비스였기 때문). 따라서 해당 설정만 추가해주도록  하였다. (best_http_host 로 설정된 것은 ingress 설정대로 Client 요청 URL 로 잘 설정 되는 것 같았다 (Ingress 자체가 도메인으로 라우팅을 해줬기 때문, 만약 서브 도메인이나 다른 IP 등으로 하는 통신이면 Host 설정 제어도 필요해보임) 

 

 

또한 이 때, 기본 설정이 $remote_addr 인 점이 의아하여, 더 살펴보니 앞단에서 받는 Forwarded 값을 믿고 그대로 사용하거나, Nginx 스스로 판단하여 새로운 값으로 넣어주거나 둘 중의 하나를 선택할 수 있는 옵션이 있었다. [use-forwarded-for] 라는 옵션인데, Ingress Nginx Controller 정의서 중 ConfigMap 에 data 부에 추가해주면 된다. 너무 길어지니 해당 블로그를 참고하길 바란다 (https://findstar.pe.kr/2021/08/22/nginx-ingress-controller-use-forwarded-for-option/). 참고로 나는 true 로 설정하여, 앞단에 load balancer 가 전달하는 Client IP 를 그대로 사용하였다. 

 

 


하지만 의문점은 더 있었다.

 

 

 

1. 현재 Base 요청에 대해 뜨는 내용 중 X-Forwarded-For 에 계속 묶여있는 10.244.1.0 은 무슨 값일까?

 

 

CNI 가 설치되면 Node 간 통신이 가능해진다. 없으면 K8s 는 노드별 Container Router IP 가 동일하여, 노드 구분을 하지 못한다

 

 


우선 내가 셋업한 클러스터였으므로 CNI 에 대해선 어느 정도 생각하고 있었으니, 조금 더 이해해보기 위해 위 그림을 참고해보자.
10.244.1.0과 2.0 은 flannel.1 의 가상 IP이다. 정확히 말하면 해당 노드의 Flannel 대역을 할당하는 가상 라우터의 IP라고 볼 수 있을 것 같다 (자신이 해당 노드 컨테이너 네트워크에서 10.244.1.0/24 범위를 담당하며, 자신이 Center 라우터 IP 에 배치되고, 그게 0으로 끝나는 이유이다)

 

 

궁금했던 점은, 내가 ingress-nginx-pod (내 Ingress 리소스를 위해 Controller 가 띄운 팟) 을 보면, A 노드에 배치되었고, 내 서비스 팟 역시 A 노드에 배치되었는데, 노드 내부 통신을 위해서는 Flannel 망 사용이 불필요한 것 아닌가 싶었고, 그 생각이 틀리진 않았던 것 같다. 사진을 첨부하진 않았지만 애플리케이션 로그를 보면, X-Forwarded-For 의 두번째 IP 가 10.244.1.0, 10.244.2.1 을 번갈아가면서 전달되는 모습을 확인했다. 이는 Load Balancer 가 Worker Node 중 하나를 선택해서 Ingress Controller 로 보내는 모습같다.

 

 

$kubectl get po {INGRESS_NGINX_POD_NAME} -n ingress-nginx -o wide 

 

 

위 명령어를 통해 NGINX POD 의 노드를 확인해보면 2번 노드라서, 10.244.2.0 의 대역에서 IP 를 할당 받았을 것이다. 따라서, 이 Pod는 같은 노드에서 보낸 요청을 수신하거나, 아니면 1번 노드에서 들어오는 요청을 수신하게 된다. 이 때, 같은 노드에서 들어올 때는 바로 컨테이너 망 (cni0) 을 사용하게 되고, 다른 노드와의 통신은 Flannel 망 (VXLAN망) 을 사용하게 되는 것으로 보인다!

 

 

 

2.  왜 Load Balancer 의 IP 는 X-Forwarded-For 헤더에 누적되어 묶이지 않는 것일까?

 


이건 처음에 들었던 의문인데, 1번 의문을 해결하면서 자연스럽게 해결되었다. 첫번째 Load Balancer 는 당연히 Client의 IP 를 Forward 해주게 된다. 두번재는 위에서 언급했듯이, Ingress Nginx Controller 의 Pod 이 수신하게 되는데, 이 때 자신은 Client 를 Flannel 혹은 CNI 와 같은 라우터들로 생각하게 된다. 그래서 Load Balancer 의 IP 가 아닌, 이들의 IP 를 X-Forwarded-For 에 적게 되는 것이다.

 

 


2. Spring Boot Application 에서 따로 설정은 안해줬는데 getRemoteAddr 에 어떻게 Client IP가 들어가는지 건가?

 

Application 단에 원래 목적이였듯이 Servlet.getRemoteAddr() 을 출력해보니 Client IP 가 잘 들어가 있었다. 다만, 이건 아까 위에서 살짝 언급했던 X-Real-IP 설정도 빼주었기 때문에, 그 어떠한 설정도 해주지 않았었다. ForwardedHeaderFilter 와 연관이 된 부분인가 싶었는데, 이 필터가 동작하는 기본 설정은 없다고 한다 (옵션이 있는데 좀 다양함). 어떻게 자동으로 Servlet 에 X-Forwarded-For 의 가장 앞 헤더가 들어가게 된건지 의문이다.  그리고 이건 아직 해결하지 못했다 ^^..

 

 

 

....

 

 

조금 복잡한 Ingress Controller 를 통해 배포를 해보니, Ingress Controller 를 어떻게 사용하는건지 조금.. 아주 조금은 감이 잡힌 것 같다. 어쨌든 결과적으론 Nginx 설정 문제였다. 해도해도 정말 어려운 것 같다. 혹시 비슷한 오류를 겪으시는 분들.. 도움이 되었으면 좋겠다 (진짜 아무리 찾아도 안나왔음.. ㅠㅠ). 이틀 넘게 같이 싸우면서 도와준 GPT 형님 감사합니다.

 

 

728x90
반응형

'Logging > 인프라' 카테고리의 다른 글

[문제기록] K3s 에서 Airflow 사용 중 Iptables 규칙 과부하로 인한 통신 불가 문제  (2) 2025.03.08
'Logging/인프라' 카테고리의 다른 글
  • [문제기록] K3s 에서 Airflow 사용 중 Iptables 규칙 과부하로 인한 통신 불가 문제
문케이크
문케이크
    반응형
  • 문케이크
    누구나 개발할 수 있다
    문케이크
  • 전체
    오늘
    어제
    • 전체 보기 (122)
      • CS 이론 (13)
        • 운영체제 (8)
        • 네트워크 (2)
        • 알고리즘 (0)
        • Storage (3)
      • Spring (26)
        • Spring 기본 (12)
        • Spring 심화 (0)
        • JPA (11)
        • Spring Security (3)
      • 리액티브 (0)
        • RxJava (0)
      • SW 설계 (14)
        • OOP (0)
        • UML (3)
        • OOAD (0)
        • Design Pattern (11)
      • Java (8)
      • 웹 운영 (17)
        • AWS (15)
        • 운영 구축 (2)
      • Testing (3)
        • Unit (3)
      • Extra (3)
        • API 적용 (1)
      • 인프라 기술 (5)
        • Kubernetes (2)
        • Elasticsearch (3)
      • Logging (7)
        • Spring (5)
        • 인프라 (2)
      • 일상 (2)
        • 음식점 리뷰 (2)
        • Extra (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

    • 문케이크의 블로그
  • 인기 글

  • 태그

    spring boot
    n+1
    mockito
    analyzer
    Setter
    OOAD
    BEAN
    디자인 패턴
    Spring
    Configuration
    SRP
    spring container
    김영한
    decorator
    OOP
    Composite
    Java
    단위테스트
    di
    JPA
    객체지향
    GoF
    junit
    composition
    k8s
    elasticsearch
    lazy loading
    lombok
    runtime exception
    Design Pattern
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
문케이크
[문제기록] K8s+Ingress 로 Spring Boot 배포시 Tomcat 400 에러 관련
상단으로

티스토리툴바