comppi 의 0.1.0 버전 개발(MVP) 이 어느정도 마무리가 되면서 드디어 미뤄왔던 운영 서버를 올리는 작업에 착수했다. 현재 생각은 24 시간 돌아가는 서버용 PC 에는 운영서버를, 개발할 때만 켜두는 개발PC 에 개발 서버를 올려서, 운영서버에는 부담이 가지 않도록 하는 것이다.
이런 경우 개발 PC에 별도의 쿠버네티스 클러스터를 만들어두고 운용하는 편이 좋을 것 같은데, 그렇게 되면 클러스터 외부에 로드밸런서를 달아야 한다. 왜냐면 난 공용 IP 가 하나이고 때문에 도메인에 따라서 어떤 클러스터로 보낼지 정해줘야 하기 때문이다. 물론 이렇게 할 수도 있지만, 일이 너무 커지는 것 같기도 하고 쿠버네티스의 여러가지 오브젝트를 다뤄보고 싶은 욕심도 있었어서, 두개의 PC 를 하나의 쿠버네티스 클러스터로 묶고 클러스터 내부에서 라우팅 시켜주는 식으로 구성하기로 했다.
그 대신 개발서버와 운영서버 리소스들의 네임스페이스를 나누어서 (comppi-dev / comppi-prod) 운용할 예정이다. 네임스페이스를 분리하면 각 네임스페이스의 리소스를 다루는 것이 용이해지며, 네트워크 정책, RBAC 같은 설정을 통해 보안을 강화할 수도 있다고 한다. 나는 특히 리소스를 물리적으로 분리하여 개발PC 에는 comppi-dev 네임스페이스를, 운영PC 에는 comppi-dev 네임스페이스를 운용할 예정이다.
1. Gateway api
Gateway api (https://gateway-api.sigs.k8s.io/) 는 기존의 Ingress, Load Balancer, Service Mesh 를 통합한 차세대 리소스로, 정식 버전(v1) 이 23년 10월에 출시된 아주 따끈따끈한 녀석이다. 원래는 트래픽 인입 및 라우팅을 별도의 쿠버네티스 리소스를 쓰지 않고 그냥 Deployment 에 Nginx 파드를 띄워서 처리 했었는데, 쿠버네티스 오브젝트인 Ingress 로 변경을 고민하다가 최종적으로는 Gateway api 를 쓰기로 했다. 워낙 새로운 기술을 쓰는데 적극적이기도 하고, 결정적으로 Ingress 는 외부의 트래픽을 클러스터 내부로 받는 역할을 하는데, Cross Namespace Routing 이 불가능 하기 때문에 내가 그리는 설계를 구현할 수가 없었기 때문이다.
아래 그림은 Gateway api 의 디자인 목표? 철학? 을 보여준다. 3 개의 페르소나(...) 를 가정하고 각자의 관심사에 따라서 그에 상응하는 리소스에만 집중하도록 했다고 한다.
각각의 주체가 의미하는 바는 다음과 같다.
- Infrastruture Providers - 인프라 구성 도구 제공자로, Nginx, Istio 같은 어플리케이션을 실제로 개발하는 주체
- Cluster Operators - 쿠버네티스 클러스터를 구축하고 운영하는 데브옵스 엔지니어
- Application Developers - 어플리케이션을 개발하는 백엔드 개발자
이들은 각각의 리소스를 개발하는데에 집중하면 된다. 즉, 나는 현재 인프라 구축과 어플리케이션 개발을 동시에 하고 있으니, GatewayClass 는 그냥 Infrastructure 제공자에 일임하면 되고 Gateway 와 Route 리소스만 잘 정의하면 된다.
2. 동작 형태
Nginx Gateway Fabric 을 설치하게 되면 GatewayClass 를 포함한 여러 오브젝트들이 배포 된다. 인입을 담당하는 Service 와 트래픽을 받는 Deployement, 그리고 이외 ConfigMap, Sa 를 비롯한 여러 CRD가 생성된다.
여기서 실질적으로 트래픽 관리를 수행하는 리소스는 당연히 Service 와 Deployment 일 것이다. Service 의 경우 Default kind 는 LoadBalancer 인데 이건 클라우드 환경에서 쓰이는 것으로, 나는 NodePort 로 변경하고 포트번호를 적절히 설정해주었다. 여기서 처음으로 트래픽을 받아 Deployment 로 넘겨준다. Deployment 를 보면 두개의 파드가 올라가 있는데 하나는 nginx 이고 하나는 nginx-gateway 이다. 처음에는 각각이 무슨 역할을 하는지 잘 몰랐는데 로그를 읽어보면 어떤 역할을 하는지 분명히 알 수 있다.
nginx 는 실제로 웹서버 역할을 하는 nginx 파드이다. kubectl exec 로 내부를 진입해보면 설정 정보도 까볼 수 있고(/etc/nginx), 로그도 기존에 쓰던 nginx 와 동일한 형태로 찍힌다.
nginx-gateway 는 처음 로드되면 클러스터에 있는 모든 Service, Endpoint, Secret 등 트래픽 라우팅에 필요한 클러스터 서비스 정보들을 읽어들이기 시작한다. 그러면서 해당 정보에 맞추어 실제 워크로드인 nginx 파드의 설정을 조정하여 원하는 상태로 동기화 시켜준다. (Upserted the resource 로그와 Reconciling the resource 로그가 반복적으로 찍힌다.) 라우팅 구성을 담고있는 HTTPRoute 도 전부 읽어들인다. 간간히 Nginx 설정 정보를 담고 있는 여러 conf 파일들의 갱신 로그가 뜨는데, 내가 설정해준 ssl 인증서도 갱신되는 것을 볼 수 있다.
그러니까 정리해보면 GatewayClass 는 Gateway 와 Route 오브젝트를 기반으로, 실질적으로 Gateway 역할을 수행하는 파드를 프로비저닝하거나 자동을 업데이트해주는역할을 담당하게 되는 것이다.
2. 프로젝트 적용 내용
나는 Nginx 의 GatewayClass 구현체 헬름차트를 다운 받고 (https://docs.nginx.com/nginx-gateway-fabric/installation/)
여기에 Gateway 와 HTTPRoute 만 정의해주었다. 아래는 내가 구성한 헬름 패키지 구조와 리소스 내용
아래 내가 작성한 메니페스토와 이에 대한 간략하게 설명이다.
Gateway.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: comppi-gateway
namespace: comppi-gateway
spec:
gatewayClassName: nginx
listeners:
- name: http
protocol: HTTP
port: 80
- name: https-api
port: 443
protocol: HTTPS
hostname: "api.comppi.site"
allowedRoutes:
namespaces:
from: Selector
selector:
matchLabels:
kubernetes.io/metadata.name: comppi-dev
tls:
mode: Terminate
certificateRefs:
- group: ""
kind: Secret
name: api-tls
- name: https-bucket
port: 443
protocol: HTTPS
hostname: "bucket.com-p.site" # 도메인 수정 필요
allowedRoutes:
namespaces:
from: Selector
selector:
matchLabels:
kubernetes.io/metadata.name: comppi-dev
tls:
mode: Terminate
certificateRefs:
- group: ""
kind: Secret
name: bucket-tls
- spec.listeners[] : gateway 에서 어떤 포트를 listen 할지 정해준다.
- spec.listeners[].allowedRoutes : 어떤 네임스페이스 또는 워크로드에서 트래픽을 받을수 있는지 허가 해준다.
* 여기서 주목할 점은 HTTPRoute 는 어플리케이션단에 붙는 리소스라는 것이다. 상기했듯 Gateway 리소스는 인프라 관리자의 관심사고, HTTPRoute 같은 리소스는 어플리케이션 개발자의 관심사이다. 트래픽 라우팅 규칙은 인프라 관리자와 어플리케이션 개발자 둘다 걸쳐있는데, 관리자측에서는 보안과 관련하여 트래픽 라우팅을 허가를 해주는 역할로 해석이 된 것. 이와 반대로 HTTPRoute 는 트래픽이 향하는(백엔드 서버가 있는) 네임스페이스에 생성이 되며, Gateway 에서 트래픽을 땡겨오는 역할을 한다. - spec.listeners[].tls : tls 암호 프로토콜을 처리해준다. Secret 에서 값을 가지고 오도록 하였다.
HTTPRoute.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: api-route
namespace: comppi-dev
spec:
parentRefs:
- name: comppi-gateway
namespace: comppi-gateway
hostnames:
- "api.comppi.site"
rules:
- matches:
- path:
type: PathPrefix
value: /
filters:
- type: RequestHeaderModifier
requestHeaderModifier:
add:
- name: X-Fowarded-Proto
value: https
backendRefs:
- name: api-gateway
port: 8000
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: bucket-route
namespace: comppi-dev
spec:
parentRefs:
- name: comppi-gateway
namespace: comppi-gateway
hostnames:
- "bucket.com-p.site"
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: minio
port: 9000
metadata, parentRef 설명은 생략 ... 레퍼런스 문서(https://gateway-api.sigs.k8s.io/reference/spec/)
- spec.rules[] : 트래픽 라우팅 관련한 규칙을 정의한다.
- spec.rules[].matches : 생략... 문서 참조
- spec.rules[].backendRefs : 트래픽을 보낼 백엔드 서버
- spec.rules[].filters : 요청 헤더등을 조작하는 필터
여기에 RequestHeaderModifier, ResponseHeaderModifier 같이 헤더를 조작하거나 RequestMirror (요청을 여러 백엔드 서버에 보내는 다중 요청) RequestRedirect(리다이렉트), UrlRewrite 등 여러가지가 있다. 여기서 라우팅 규칙을 꽤 섬세하게 다룰 수 있다.
초반엔 간단하게 라우팅 규칙만 설정해두려고 했는데 예상치못한 CORS 에러를 겪고, 상당히 시간을 소모했다.
Cross Origin Resource Sharing 은 교차출처리소스공유의 줄임말로, 서버와 요청 Origin 이 다른 경우 리소스를 공유할지 말지에 대한 정책이다. CORS 정책은 프리플라이트라는 소통 체계를 통해 적용되는데, 클라이언트가 서버로 프리플라이트를 보내면 서버에서 해당 클라이언트의 Origin 을 확인하고 접근 가능한지를 답해주는 식이다. 그러니까 서버에서 프리플라이트 요청에 "Access-Control-Allow-Origin" 헤더를 적절하게 보내주면 해결된다.
문제는 도무지 해당 헤더를 보낼 생각을 안하는 것이었다. 분명 allow 되어있는 origin 인데 계속 cors 에러를 내뱉었다. 그냥 nginx 에서 ResponseHeader 를 조작하여 프론트 도메인으로 응답을 줄까도 생각했는데 그러면 스웨거를 사용하지 못하게 되니 개발 단계에서 문제가 생긴다. 고민하다가 기존에 설정된 nginx 설정을 까봤다.
아래처럼 백엔드 서버에 호스트, 요청지 아이피 주소, 프록시 서버 아이피 주소, 프로토콜을 헤더에 붙여서 보내고 있었다.
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;
곰곰히 생각해보니까 CORS 에는 요청 프로토콜도 규칙으로 작용하는데(https 를 허용해 준경우 http 를 받지 못함) 이게 문제인가 싶어서 아래 처럼 헤더 추가 필터를 추가해 주었다. (X-Forwarded-Proto 는 프로토콜을 포워딩 해준다는 의미의 헤더이다)
filters:
- type: RequestHeaderModifier
requestHeaderModifier:
set:
- name: X-Forwarded-Proto
value: $scheme
근데 이렇게 설정하니 아예 라우팅이 되지 않았다. 컨테이너 내부에 들어가서 nginx 설정 정보를 열어 보니 라우팅 블록이 아예 삭제가 되고 invalid-backend-ref 라는 업스트림이 대신 정의 되어있다(...) 여러 시행착오 끝에 변수를 직접 사용하는 것은 불가능 하다는 것을 알게되었다.
그러다가 어차피 nginx 앞단에서 메세지를 받을 때 tls 복호화가 진행되지 않으면 백엔드 서버에 접근 자체를 할 수 없으니, https 라고 그냥 문자열로 명시해도 괜찮을 것 같다는 생각이 들었다. 그냥 문자열로 넣어보니 cors 에러가 해결이 되긴 했다.
# 수정 적용된 nginx
proxy_http_version 1.1;
proxy_set_header X-Forwarded-Proto "https";
proxy_set_header Host "$gw_api_compliant_host";
proxy_set_header X-Forwarded-For "$proxy_add_x_forwarded_for";
proxy_set_header Upgrade "$http_upgrade";
proxy_set_header Connection "$connection_upgrade";
proxy_pass http://comppi-dev_api-gateway_8000$request_uri;
그런데 솔직히 찝찝한 구석이 있어가지고 set 대신 add 태그로 https 를 넣어봤는데
proxy_http_version 1.1;
proxy_set_header X-Forwarded-Proto "${x_forwarded_proto_header_var}https";
proxy_set_header Host "$gw_api_compliant_host";
proxy_set_header X-Forwarded-For "$proxy_add_x_forwarded_for";
proxy_set_header Upgrade "$http_upgrade";
proxy_set_header Connection "$connection_upgrade";
proxy_pass http://comppi-dev_api-gateway_8000$request_uri;
## 중략
map ${http_x_forwarded_proto} $x_forwarded_proto_header_var {
default '';
~.* ${http_x_forwarded_proto},;
}
이렇게 수정이 되는걸 봐서는 제대로 동작을 해야 맞는 거 같은데 오류가 아닌가 하는 생각이 들었다.
이 부분은 어떻게 깔끔하게 처리를 할 수 있을지 모르겠어서 Nginx Gateway Fabric 깃허브에 Discussion 을 을 남겼다.
링크( https://github.com/nginxinc/nginx-gateway-fabric/discussions/2529)
=> 가장 최근 버전(Edge 버전) 에서는 기본적으로 X-Forwarded-Proto 헤더에 $scheme 으로 설정되도록 수정되었다고 한다. 그래서 다음 버전을 써보라고 함. 빠르게 답변이 와서 놀랐다.
4. 추후 숙제
몰랐는데, 작성하면서 보니까 Gateway api 를 이용해서 서비스 메쉬까지 구성할 수 있다고 한다. 아직 서비스 메쉬는 써보지 않았는데, 서비스 간 서킷브레이커나 로드밸런스(이런건 굳이 아직 필요하진 않겠지만) mTLS 같은 서비스간 ssl 통신같은걸 해줄 수 있다고 한다. 나중에 기회가 되면 구현해보고 싶다. 보안에도 요새 관심이 많아져서 클러스터 내부에서의 보안도 더 공부해서 올려야지...