쿠버네티스를 공부하다 보면 아마 처음으로 접하게 되는 것 트래픽 관련 리소스가 Service라고 하는 것일텐데, 이는 우리가 생각하는 대로 특정한 Pod에 대하여 IP를 뚫어주어 전략(ClusterIP, NodePort, LoadBalancer)에 따라서 직접 접근할 수 있게 된다. 그다음으로는 Ingress라는 리소스도 있는데, 이 또한 Port를 이용하여 IP 계층에서 직접 트래픽을 조작해 준다.
대체 어떠한 차이점이 있을까? 쿠버네티스에 대해서 단순하게 이해하고 있을 때에는 Ingress는 프론트엔드 페이지 보여줄 때, Service는 그 외라고 알고 있었지만, 좀 더 공부해 보면 사실 굉장히 이야기할 거리가 많은 주제이고 복잡하기도 했다. 재미있는 사실은 결국에는 Ingress는 웹을 보여줄 때, Service는 그 외라고 생각하면 된다.
나도 사실 강의 같은데에서 Ingress는 프론트에 Service는 백에 쓰라고 하는 내용을 들어서 대충 그렇게만 이해하고 넘어갔던 것 같은데, 자세히 공부해 보니까 다양하고 재미있는 이유가 있었다. 하지만 결론적으로는 인그레스는 웹에, 나머지는 서비스를 쓴다라는 느낌으로 이야기가 끝나는 데, 뭔가 위와 같은 그림과 같았다.
개발뿐만 아니라 다양한 분야에서 이러한 양상이 나타나겠지만, 쿠버네티스에 있어서도 이런 것 같다. 팽도리 시절에는 그냥 대충 그렇게만 알고서 있다가 어느 정도 공부를 해보면 팽태자처럼 뭐가 뭔지 어떻게 설명할 수 있는 정도가 되는 듯하다. ( 엠페르트처럼 고인 사람들은 그냥 별생각 없는 듯 )
최근에 쿠버네티스에 대한 공부를 깊게 해 보면서 팽태자 정도의 설명 정도는 할 수 있을만할 것 같고, 무엇보다 Ingress와 Service에 대해서 자세히 비교되어 있는 내용을 찾기 어려웠기에 직접 다이어그램을 그려가며 설명을 해보려고 한다. 이번 포스팅에서는 Service에 대한 설명부터 시작해 Ingress까지 이어지면서 이야기가 전개되게끔 하려고 했으니, 필력이 많이 부족할지라도 이 점 유의해서 읽어줬으면 한다.
본론
먼저 service와 ingress에 대해 개략적인 설명을 하고 넘어가자. 가장 중요한 이 둘의 공통점은 라우팅이다. 이 두 리소스는 쿠버네티스에서 트래픽을 제어하는 데 사용되며, 우리가 아는 그 서버와 서버 간의 통신을 구현하기 위해서는 없어서는 안 되는 존재이다. 쿠버네티스 클러스터를 구축하고 하나의 프로덕트를 배포하는 과정에서는 이 두 가지 리소스를 전부 사용할 것이다.
그렇다면 어떻게 이 두 차이가 다를까? 왜 결론적으로는 웹서비스에는 인그레스, 나머지는 서비스를 쓰라고 하는 것일까? 먼저 클러스터 내부와의 통신에 대해서 이야기하면서 외부 간의 통신에 대해서 이야기하는 순서로 진행하려고 한다.
Service
먼저, Service(이하 서비스)는 클러스터 내부에서 안정적인 네트워크 엔드포인트를 제공하기 위한 리소스이다. 이러한 서비스는 추상화된 객체이다. 다이어그램으로 나타내면 아래와 같이 특정 Pod의 포트를 뚫어주어 클러스터 내부의 다른 Pod( 예를 들면, 스프링부트 같은 서버들이 있겠다 )에서 접근할 수 있도록 도와주는 역할을 한다.
하지만 대부분의 다이어그램에서 위와 같이 설명을 하지만, 후술 할 Ingress에 대해서 설명하기 위해서 좀 더 다른 관점을 제공하고자 한다. 간단하게 다이어그램으로 나타낼 때에는 위처럼 표현한다. 하지만 서비스는 사실 특정 Pod에 의존하여 딱 달라붙어 포트를 노출시켜 주는 리소스가 아니다. 서비스의 역할을 생각해 보았을 때, 어떻게 작동해야 하는지 생각해 보자. Pod는 새로 뜰 때마다 새로운 IP를 할당받는다. 그렇기 때문에 특정 Pod에 대해서 IP와 Port를 외부로 노출시킨다고 끝이 아니라, 레플리카셋에 의해 새롭게 뜨는 Pod에 대해서 트래픽을 항상 새로운 Pod에 물려줘야 한다.
그렇다면 이게 어떻게 가능할까? 서비스는 왜 추상적인 객체라는 것이고 어떻게 구현되어 있는 걸까? 정답은 Service라고 하는 쿠버네티스 리소스는 특정 노드에 귀속되지 않는다. 클러스터의 전체에 있는 것이다. 서비스는 그저 각 노드에 라우팅 테이블을 만드는 것이다.
서비스 워크로드를 배포하게 되면, 위와 같이 각 노드에 라우팅 테이블이 만들어진다. 이를 통하여 모든 요청이 nginx-pod를 잘 찾아갈 수 있도록 트래픽을 제어한다. 다른 노드에 있는 Pod에서도 nginx-pod의 80번 포트에 접근하고자 할 때 접근할 수 있게 된다.
이러한 라우팅을 돕는 게 Kube-Proxy이다. 서비스는 각 노드에 이러한 설정을 kube-proxy의 도움을 받아 전파하여, 나중에 다른 노드의 혹은 다른 팟에서 접근할 일이 있다면 위 라우팅 테이블에 따라서 서버끼리 통신할 수 있는 것이다.
이렇게 서비스는 동작하며 Service는 다음과 같이 간단하게 yaml로 작성할 수 있다.
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
selector:
name: my-nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
중요한 점은 selector를 이용한다는 점이다. Pod을 배포할 때, 일반적으로 Selector를 명시하여 배포해 주는데, 이러한 Selector에 맞춰 지정해 줘야 Service가 제대로 타겟을 이해하고 서비스를 적용할 수 있게 된다.
외부에서의 접근? 클러스터의 내부?
후술 할 ServiceTypes이나 Ingress에 대해서 설명하기 전에 먼저, 외부와 내부에 대한 정의를 제대로 짚고 가려고 한다. 아래와 같은 상황을 생각해 보자.
먼저 node01에 있는 nginx-pod을 생각해 보았을 때, 해당 노드 pod을 기점으로 다양한 부분에서 접근을 시도할 수도 있겠다. 같은 노드지만 Pod의 외부에 있는 node라는 pod도 있을 것이고, 다른 노드에 있는 spring이나 redis도 있을 것이다. 뿐만 아니라 cluster 외부에서도 접근을 생각해 볼 수 있을 것이다.
위 그림을 베이스로 디벨롭해 가면서 다양한 내용에 대해서 설명하려고 한다. 위 그림을 생각하면서 Pod를 기점으로 내부와 외부는 어딘지 생각해 보면서 따라오면 좋을 것 같다.
Cluster DNS
서비스 워크로드를 배포하면 쿠버네티스 클러스터 내에서 접근할 수 있게 된다. 예를 들어 위와 같은 형식으로 배포된 nginx-pod에 Service를 배포하였다. 그런 상황에서 node02에 있는 spring pod이 접근하기 위해서는 아래와 같이 연결할 수 있다.
host: redis.redis.svc.cluster.local
이런 식으로 POD_NAME.NAMESPACE.svc.cluster.local을 입력하는 것으로 접근할 수 있게 된다. 쿠버네티스에서는 이렇게 CoreDns라는 것을 통해서 리소스를 관리한다. 만약에 spring pod가 아니라, 같은 노드에 같은 네임스페이스에 있는 node에서 접근하는 경우에는 아래와 같이 접근하는 것도 가능하다.
curl http://nginx-pod
하지만 이러한 같은 네임스페이스, 같은 노드가 아닌 경우에는 보다 다양한 정보를 명시해 줘야 클러스터 내부의 다른 외부에서 찾아갈 수 있다. 아래와 같이 다양한 테이블이 생기게 되며, Pod의 위치에 따라 다르게 호스트명을 입력해서 접근할 수 있는 것이다.
redis까지 service(svc)를 통해서 배포했다고 생각하면 이와 같은 구조로 kube dns에 등록된다. 스프링의 입장에서 봤을 때에는 nginx에 접근하기 위해서는 nginx-pod.nginx.svc.cluster.local로 연결하면 해당 pod의 IP Address를 통해서 연결할 수 있고, redis에 연결하고자 하는 경우에는 redis.redis.svc.cluster.local로 연결해야할 것이다.
이에 대한 이야기는 여기까지만 해서, 이런 식으로 Service를 통해서 배포하는 경우에 kube DNS의 힘을 빌려서 DNS를 통해서 접근할 수 있다. Pod는 새로 뜰 때마다 IP가 바뀌기 때문에 이렇게 host 명을 지정해 놓기만 하면 되는 것이다. 이것이 클러스터 내부에서 접근하는 방식이고 Service를 선언할 때, 별도의 선언을 하지 않으면 이렇게 클러스터 내부에서 접근하는 것으로 배포되는 타입을 ClusterIP 타입이라고 한다. 이것이 기본값인데, 이 외에도 다양한 타입들이 있다. 다음으로는 ServiceTypes에 대해서 알아보자.
서비스 퍼블리싱 ( ServiceTypes )
서비스를 통해서 클러스터 내부에서 접근하는 방법에 대해서 다뤄보았다. 하지만 다양한 시나리오를 고려하여 쿠버네티스 클러스터의 밖에서도 접근할 수 있게 할 수 있지 않을까? 그럴 때 사용하는 것이 바로 이 ServiceTypes으로, 다음과 같이 세 가지의 타입을 지정해줄 수 있다.
- ClusterIP : 서비스를 클러스터 내부-IP에 노출시킨다. 이 값을 선택하면 클러스터 내에서만 접근할 수 있다. ( 기본값!! )
- NodePort : 고정 포트로 각 노드의 IP에 서비스를 노출시킨다.
- LoadBalancer : 클라우드 공급자의 로드 밸런서를 사용하여 서비스를 외부에 노출시킨다.
이 중에서 로드 밸런서 유형은 말 그대로 로드 밸런싱을 통해서 다양한 Pod에 트래픽을 분산시켜주는 타입이고, ClusterIP와 NodePort에 대해서 소개한다. 그 후에 NodePort와 비슷하면서 다른 Ingress에 대해서 소개하려고 한다.
ClusterIP
ClusterIP는 기본값으로 따로 설정해주지 않는다면 기본적으로 이 설정이 적용된다. docker container를 띄울 때, 따로 포트바인딩을 진행해주지 않으면 외부(host)에서 직접 접근할 수가 없다. 이와 같이 Pod이 뜨지만, 클러스터 내부에서 접근할 수 없는데, 이를 뚫어주는 역할을 한다.
위와 같은 느낌이다. 이렇게 그저 80번 포트로 띄우면 내부에서만 알고 있다. docker에서 포트 바인딩을 해주듯이 Service를 통해서 port와 targerPort를 지정해줌으로써 외부에서 접근할 수 있게 해준다.
예시를 좀 더 들면 프레임워크에서 접근해야하는 데이터베이스(mysql, postgres) 그리고 인메모리 데이터그리드 ( redis, valkey ) 혹은 메시지큐 ( celery, rabbitMq ) 등이 있겠다. 이러한 팟을 띄우는 경우에 service를 노출시켜 항상 클러스터 내부의 Pod에서 접근할 수 있게 해주는 역할을 하는게 ClusterIP이다.
마지막으로, 이러한 접근 방법에 Kube DNS를 통해서 redis.redis.svc.cluster.local로써 접근할 수 있게 해주는 방법이 바로 ClusterIP 이다.
NodePort
다음으로는 NodePort이다. 그림과 같이 type을 NodePort로 지정해주게 되면 이제 정말 외부 그 자체인 클러스터 바깥에서도 접근할 수 있게 된다. 참고로 쿠버네티스는 클러스터 외부에서 접근하는 경우를 위해 포트는 30000-32767 만을 할당한다.
이 부분은 미세한 상식인데, 먼저 0 ~ 1023 포트는 HTTP나 HTTPS 등 이 외에 다양한 Well-known 포트를 위해서 사용하기 때문에 앞 부분은 제한한다. 그렇게 30000번 포트에서부터 시작하는데, 보통은 뒷 자리를 0000으로 만들었기 때문에 기존에 사용하는 포트 + 30000으로 설정한다. 예를 들어 80번 포트를 사용하는 서비스를 NodePort로 열어버리면 30080번 포트로 접근할 수 있게 해준다.
그 후 32767까지만 할당하는 이유는 ephemeral port로 인해서이다. 이는 리눅스 시스템에서 서버 애플리케이션과 연결을 설정하는 데 사용되는 임시 네트워크 엔드포인트를 말한다. 이를 통해 32768부터 65535까지는 임시 포트를 위해 지정되어 있기 때문에 쿠버네티스 클러스터는 그 바로 앞인 32767 까지만 랜덤으로 할당한다. 또는 직접 spec.ports.nodePort를 설정해서 원하는 포트를 할당해줄 수 있다. 이 경우는 30000-32767을 벗어나는 경우의 할당까지 가능하다.
각설하고, 이렇게 NodePort를 지정하게 되면 외부에서 접근하게 할 수 있다. 스크립트는 아래와 같다.
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
type: NodePort
selector:
name: nginx
ports:
- port: 80
targetPort: 80
# nodePort: 30080 지정하지 않아도 되는 부분, 랜덤 할당됨
하지만 이렇게 접근하게 되는 경우 외부에서 http://<노드IP>:<NodePort>의 형태로 접속해야 하므로, 사용자가 접근하기 힘들다. DNS를 통하여 A 레코드를 통해 해당 쿠버네티스 클러스터를 바라보게 하면 그나마 괜찮지만, 도메인 네임을 입힐 수 있지만, 어찌됐던 뒤에 포트를 붙여야 한다는 사실은 변하지 않는다.
이러한 설정을 고려하여 외부에서 접근시키고 싶은 경우에 NodePort를 사용하는 것이 적절할까? 항상 모든 기술에는 장단점이 있기에 시나리오 마다 다르지만, 대부분의 경우에서 Ingress를 쓰는 것이 편하다. 그렇다면 이제는 Ingress에 대해서 알아보자.
Ingress
Ingress(이하 인그레스)는 클러스터 외부에서 내부 서비스로 들어오는 HTTP(S) 트래픽에 대한 규칙 기반 라우팅을 정의하는 쿠버네티스 리소스이다. 인그레스는 자체적으로 트래픽을 라우팅할 수 있는 기능만 명세하고, 실제로 동작하기 위해서는 인그레스 컨트롤러가 필요하다.
Service를 쓰는 경우 http://<노드IP>:<NodePort>과 같이 접속해야 했던 것에 비해 Ingress는 80번 포트를 30000번대 포트로 라우팅 시켜줌과 동시에 다양한 설정을 입힐 수 있다. HTTP에서 HTTPS로 SSL도 구현해주는 기능도 있고, L7 로드밸런서로도 볼 수 있다.
Ingress에서 가장 중요한 것은 두 가지를 설정해줘야 한다는 점인데, 첫 번째는 Ingress resource이다. 이는 우리가 아는 쿠버네티스 매니페스트대로 정의해주고 등록해줘야 한다는 뜻이고, 다음으로는 Ingress가 사용할 Ingress controller를 사용해야 한다는 점이다. 인그레스 컨트롤러가 뭐지? 라고 생각할 수도 있겠지만 대표적인 예시는 아래와 같고, 나열해보면 뭔지 감이 올 것이다.
- Ingress Controller : GCP LoadBalancer, Nginx, Contour, HAproxy, traefik, Istio etc..
위와 같은 컨트롤러와 함께 배포되는 것이 인그레스는 우리가 아는 다양한 도메인에 대해서 손쉽게 라우팅을 해준다. example.com을 예로 들자면 game.example.com 혹은 drive.example.com 등 다양한 서브도메인을 라우팅 시켜줄 수도 있고, 혹은 Path에 대해서도 라우팅 시켜줄 수도 있다. example.com/movie나 example.com/event 등이다.
이렇게 Ingress는 특정한 컨트롤러를 사용해줌으로써 트래픽을 컨트롤 할 수 있게 된다.
Path를 설정하는 경우
먼저 Path를 설정하는 경우 다음과 같이 Ingress-path.yaml 을 작성할 수 있다. 예를 들어 www.example.com/book이나 www.example.com/coffee등으로 인그레스를 설정하고 싶은 경우에는 아래와 같이 작성할 수 있다.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-path
spec:
rules:
- http:
paths:
- path: /book
pathType: Prefix
backend:
service:
name: book-service
port:
number: 80
- path: /coffee
pathType: Prefix
backend:
service:
name: coffee-service
port:
number: 80
인그레스를 통해서 위와 같이 설정하여 path를 설정해줄 수 있으며, default로 다른 path로 보내는 것도 가능하다. 그렇기 때문에 유효하지 않은 path에 대해서 404 에러 페이지를 제공하는 등의 설정도 해줄 수 있다.
Subdomain을 설정하는 경우
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-subdomain
spec:
rules:
- host: book.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: book-service
port:
number: 80
- host: coffee.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: coffee-service
port:
number: 80
서브도메인을 달아줘야 하는 경우에는 위와 같이 설정할 수 있다. book.example.com이나 coffee.example.com을 통해서 해당 서버에 접근할 수 있게 된다.
Ingress Controller 설정
인그레스는 앞서 말했듯이 위와 같은 쿠버네티스 매니페스트를 작성하는 것은 기본이며, 서드파티 인그레스 컨트롤러를 설정해서 라우팅을 진행한다고 이야기했었다. 그렇다면 어떻게 그러한 설정을 해줄 수 있을까?
먼저, 쿠버네티스 매니페스트를 이용하여 nginx를 사용하는 방법은 아래와 같다. ingress-nginx 깃허브에서 제공하는 매니페스트를 적용하기 위해 아래의 스크립트를 이용한다.
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/cloud/deploy.yaml
그 후, 우리가 앞서 배포했던 Ingress 매니페스트를 수정하여 다음과 같이 어떤 써드파티를 사용할 것 인지 정의해줄 수 있다.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-ingress
annotations:
kubernetes.io/ingress.class: "nginx" # Nginx Ingress Controller 사용을 명시
spec:
rules:
- host: book.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: book-service
port:
number: 80
위와 같은 과정을 통해서 nginx와 같은 인그레스 컨트롤러를 등록해줄 수 있고, 반드시 이러한 기능이 필요하다. 쿠버네티스를 관리하면서 Helm chart를 쓴다면 보다 손쉽게 명령어로 설치할 수도 있다.
마치며
결국 프론트엔드 페이지로 보낼 때에는 Ingress, 그리고 백엔드와 통신하는 경우에는 Service를 쓰는 것이 일반적이긴 하다. 하지만 시나리오에 따라서 이러한 자세한 내용을 알지 못하고 있다면 쿠버네티스 클러스터를 확장하는 데에 있어서 허술한 부분이 드러날 것 같다.
최근에 CKA를 공부하게 되면서 다양한 쿠버네티스의 개념들에 대해서 공부하고 있는데, 생각보다 재미있는 내용도 많았다. 특히 리눅스 기반으로 슈퍼컴퓨터 클러스터를 관리하는 일을 하면서 리눅스에서의 네트워크는 되게 잘 알고 있다고 생각했지만 배우면서 몰랐던 내용도 꽤 알 수 있었고 너무 재미있었다.
CKA를 치기 위해서는 여권이 필요한데, 재발급까지 약 한달이 걸린다고 하니.. 그 동안 포스팅을 작성하면서 열심히 좀 더 공부를 해봐야할 것 같다.
참고
- [coursera] 임시 포트란 무엇인가요? : https://www.coursera.org/articles/ephemeral-ports
- [kubernetes] 서비스 : https://kubernetes.io/ko/docs/concepts/services-networking/service/
- [kubernetes] 인그레스 : https://kubernetes.io/ko/docs/concepts/services-networking/ingress/
감사합니다.
'DevOps > kubernetes' 카테고리의 다른 글
쿠버네티스의 Kustomize에 대해서 (1) | 2024.12.20 |
---|---|
ETCD Leader Election이란? (1) | 2024.12.18 |
로컬 쿠버네티스 클러스터 환경 세팅 ( feat. minikube, lens ) (1) | 2024.12.16 |
[K8s homeserver 구축 - 3] github actions를 통한 CI (0) | 2023.11.10 |
[K8s homeserver 구축 - 2] 라즈베리파이 K3s 클러스터링 (0) | 2023.11.09 |