티스토리 뷰

Outline

이번 글에서는 서비스를 외부 클라이언트(사용자) 들에게 노출 시키는 다양한 방법에 대하여 다뤄보도록 하겠다.

-

Definition

서비스를 외부 클라이언트에게 노출 시키는 방법은 총 세가지 다.

  1. 서비스의 타입을 NodePort 로 세팅한다.
  2. 서비스의 타입을 Load Balancer로 세팅한다.
  3. Ingress Controller란 오브젝트를 생성해 하나의 아이피로 여러 서비스를 노출 시킨다.

-

위 세가지 방법을 하나씩 알아보도록 하겠다.

-

NodePort Service

Node Port 구성도

개별 클러스터 노드의 특정 포트를 열어서 해당 포트를 통해 트래픽을 전달받는 것을 말한다.

노드 포트 타입의 서비스를 생성하면 쿠버네티스는노드 전체에 해당 포트를 예약한다.

예를들어, 노드 포트 서비스가 포트로 30000번을 선택한다면 쿠버네티스는 클러스터 내 노드들의 30000번 포트를 예약한다.

결론적으로, 외부 클라이언트는 서비스의 내부 클러스터 ip와 노드 아이피 및 예약된 포트를 통해 접근할 수 있다.

그럼 직접 노드 포트 타입의 서비스를 생성해보자.

apiVersion: v1
kind: Service
metadata:
  name: jordy-node-port-svc
spec:
  type: NodePort
  ports:
  - port: 80
    targetPort: 8080
    nodePort: 30000
  selector:
    app: jordy

위 YAML을 설명하자면

  • port: 80 # 서비스의 내부 클러스터 아이피의 포트
  • targetPort: 8080 # 뒷편에 위치한 포드의 포트
  • nodePort: 30000 # 개별 클러스터 노드에 접근할 수 있도록 오픈한 노드의 포트

-

위 코드로 yaml을 생성한 다음 아래 명령어 양식을 참조해 서비스를 생성하자.

kubectl create -f <file-name.file-extention>

서비스 생성 후에는 서비스 뒷편에 있는 포드로 접근할 수 있는 경로는 2가지가 된다.

  • 서비스 IP 를 통한 접근
  • 특정 노드의 IP:30000

-

서비스의 클러스터 IP는 서비스 클러스터 내부에서 사용되는 아이피이고 외부에 오픈되는 아이피는 별도로 없기 때문에 어차피 이전과 동일하게 내부 클러스터 IP로 접근하는 것은 불가능하다.

그럼 새롭게 생긴 경로인 <specific cluster node IP>:30000을 통해 접근해보자.

-

우선 노드 클러스터 IP를 확인하자.

kubectl get node -o wide

이제 kubectl get <kind of object> 형태의 명령어는 익숙해졌을 것이다.

기본 네임 스페이스 내에 전체 노드를 조회하는 명령어인데, 옵션을 붙이지 않으면 내부 아이피와 외부 아이피가 제공되지 않으므로 -o wide 란 옵션을 붙였다.

NAME                                   STATUS   ROLES    AGE   VERSION          INTERNAL-IP        EXTERNAL-IP     OS-IMAGE                             KERNEL-VERSION   CONTAINER-RUNTIME
gke-jordi-default-pool-08313642-c93c   Ready    <none>   38d   v1.13.10-gke.0   10.000.000.000    34.000.000.000    Container-Optimized OS from Google   4.14.137+        docker://18.9.7
gke-jordi-default-pool-6a478366-mw0p   Ready    <none>   38d   v1.13.10-gke.0   10.000.000.000    34.000.000.000    Container-Optimized OS from Google   4.14.137+        docker://18.9.7
gke-jordi-default-pool-f0a197fa-stvp   Ready    <none>   38d   v1.13.10-gke.0   10.000.000.000    34.000.000.000    Container-Optimized OS from Google   4.14.137+        docker://18.9.7

위 결과를 보면 여러 컬럼이 있지만 내부 아이피와 외부 아이피가 있는 것을 확인할 수 있다. 아이피는 보안을 위해 2-4번째 자리는 0으로 처리했다.

curl을 날려보자

curl 34.000.000.000:30000
그럼 아래와 같은 결과가 리턴된다.

curl: (7) Failed connect to 34.000.000.000:30000; 연결이 거부됨

노드 포트를 통한 직접적인 접근은 보안적인 측면에서 위협이 될 수 있어 기본적인 http(80), https(443) 포트 등을 제외한 포트는 GKE 측에서 막고 있다.

그래서 방화벽 정책을 생성하여 30000번 포트에 대한 접근을 허가하도록 처리하자. 온프리미스 및 다른 클라우드 서비스과 관련한 처리는 추후에 다루도록 하겠다.

-

form.

gcloud compute firewall-rules create <rule-name> --allow=tcp:<port-number>

example.

gcloud compute firewall-rules create jordy-node-port-rule --allow=tcp:30000

-

그 후에 다시 curl을 날려보면 연결 거부 메세지가 리턴되지 않는다.

-

Load Balancer Service

Load Balancer 구성도

로드 밸런서 타입은 그 자체로 고유하고 외부에서 접근 가능한 아이피를 가지며, 서비스로 들어오는 요청들은 뒷단에 있는 포드들에 리다이렉트를 해주는 역할 한다. 따라서 클라이언트가 포드에 요청을 보내기 위해서는 서비스가 제공하는 외부 아이피로 접근하면 된다.

-

로드 밸런서 타입의 서비스를 온-프리미스로 구축하는 것이 어렵다는 의견이 많다.

그래서 어떤 점이 어렵고, 이를 보완하는 방법이 무엇인지는 추후에 다른 글에서 다루겠다.

-

아래 내용으로 YAML을 작성한 후 kubectl create -f <file-name.file-extention> 을 실행해보자.

apiVersion: v1
kind: Service
metadata:
  name: jordy-lb-svc
spec:
  type: LoadBalancer
  ports:
    - port: 80
      targetPort: 8080
  selector:
    app: jordy

-

그 후에 아래 명령어로 추가된 서비스 정보와 외부 아이피를 확인한 후 테스트를 해보자.

kubectl get svc

NAME                         TYPE           CLUSTER-IP        EXTERNAL-IP     PORT(S)          AGE
dev-svc-jordy                ClusterIP      10.000.000.000    <none>          80/TCP           4d6h
jordy-lb-svc                 LoadBalancer   10.000.000.000    34.000.000.000  80:31999/TCP     68s
jordy-node-port-svc          NodePort       10.000.000.000    <none>          80:30000/TCP     52m

로드밸러서 타입이 노드포트 타입의 확장된 형태인 만큼 로드밸런서 타입도 31999란 노드 포트가 자동으로 부여되어 설정되어 있음을 알 수 있다. 하지만 부여가 되더라도 방화벽 정책에 31999란 포트가 등록되어 있지 않으므로 연결 요청은 거부된다.

34.000.000.000 아이피를 웹 브라우저에 입력하거나, curl을 날리면 해당 서비스에 selector로 설정된 Pod들의 호스트명을 확인 할 수 있을 것이다.

웹 브라우저는 keep-alive connection을 사용하므로
연결이 계속되는 동안에는 같은 포드로 연결되는 반면,

curl은 매 명령어마다 새 연결(Connection)을 만들어내므로
Pod의 이름이 달라지기도 한다. 물론 우연의 일치로 같은
포드의 이름이 연이어 나오기도 한다.

노드 포트 서비스의 확장 형태이다.

Ingress

Ingress 원리
Ingress 구성도

개별 서비스들이 자기 소유의 공공 아이피를 가지는 반면, 인그레스는 오직 한 개의 아이피만 있으면 수십개의 서비스를 묶을 수 있다.

만약에 클라이언트가 인그레스 쪽으로 HTTP 요청을 보내면 HOST와 PATH를 바탕으로 적절한 서비스에게 포워드 해준다.

인그레스는 Application 계층에서 활동하므로 Transport 계층인 서비스가 못하는 쿠키 기반의 세션 친숙화 기능 등을 사용할 수 있다는 장점이 있다.

참고로 인그레스는 클러스터 내부에서 작동한다.

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: jordy-ing #Ingress의 고유의 이름
spec:
  rules:
  - host: sample.jordy.com # Ingress가 해당 도메인을 서비스에 매핑함
    http:
      paths:
      - path: / # 해당 패스로 들어온 데이터를 아래 기재된 이름의 서비스 내 80번 포트로 리다이렉트 한다.
        backend:
          serviceName: jordy-node-port-svc
          servicePort: 80

아래 명령어로 잘 생성됐는지 확인해보자.

kubectl get ingress

kubectl get ing

NAME              HOSTS             ADDRESS         PORTS   AGE
basic-jordy-ing   basic.jordy.com   34.000.000.000  80      3h18m

curl <ip 주소> 를 날려보면 아래와 같은 결과가 나온다,

default backend - 404

-

위 YAML에서 호스트 주소를 매핑했기 때문이다. 아래 명령어를 날려보면 404가 나오게 된다.
curl http://basic.jordy.com

-

그래서 리눅스 기준으로 로컬 DNS 추가가 필요하다.

vi /etc/hosts

# hosts 파일 내 라인 추가.
34.000.000.000    basic.jordy.com

그 후에 다시 아래 명령어를 보내면 응답이 온다.

curl http://basic.jordy.com

-

Example 1 - Ingress exposing multiple svc on different hosts.

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: basic-niniz-ing
spec:
  rules:
  - host: jordy.niniz.com
    http:
      paths:
      - path: /
        backend:
          serviceName: jordy-node-port-svc
          servicePort: 80
  - host: scappy.niniz.com
    http:
      paths:
      - path: /
        backend:
          serviceName: scappy-node-port-svc
          servicePort: 80

위 YAML로 Ingress를 생성한 후에 아래 세 가지을 반드시 확인해주어야 한다.

  1. jordy-node-port-svc, scappy-node-port-svc 서비스 생성
  2. 해당 서비스의 요청을 처리해줄 Pod 생성
  3. vi /etc/hosts 를 통해 Local DNS 생성

지금까지 학습 및 실습을 잘해왔다면 충분히 위 세가지를 할 수 있을 것이다.

-

Example 2 - Ingress exposing multiple svc on different path and same host.

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: basic-niniz-ing-multi-path
spec:
  rules:
  - host: niniz.com
    http:
      paths:
      - path: /jordy
        backend:
          serviceName: jordy-node-port-svc
          servicePort: 80
      - path: /scappy
        backend:
          serviceName: scappy-node-port-svc
          servicePort: 80

위 YAML로 Object 생성 후에 예제 1번에서 체크 항목 3가지를 확인한 다음에 curl로 테스트 해본다.

-

Additional 1 - externalTrafficPolicy 속성

서비스의 속성 중 externalTrafficPolicy 가 있다. 해당 속성은 서비스가 노드 포트를 통해 연결 요청을 받았을 때 서비스 프록시가 노드 내 포트 중 로컬에서 작동 중인 포드들에게 리다이렉트 해주는 옵션이다.

그래서 노드 내 포드 중 일부가 원격지에 있을 경우 추가적인 홉을 방지하고, 원격지 포드로 이동간 생길 수 있는 SNAT(요청자 아이피 변조)가 발생하지 않아, 요청자 아이피가 필요한 앱에서 생길 수 있는 문제를 방지 할 수 있다.

하지만 이 옵션도 단점은 있는데, 만약에 해당 노드 내 포드가 아무것도 없으면 요청을 처리할 수 없게 된다. 또한 포드 수와 무관하게 노드 갯수 만큼 균일하게 트래픽이 분배되므로 포드 배분이 비대칭일 경우 결론적으로 요청 처리에 비효율이 생긴다.

해당 내용에 대한 이해가 어려워 대략적인 내용을 작성했으며 추후에 보강하도록 하겠다.

관련 내용이 궁금하면 아래 링크를 참조하자.

https://www.asykim.com/blog/deep-dive-into-kubernetes-external-traffic-policies

-

Additional 2 - Configuring Ingress to handle TLS traffic

지금까지 Ingress를 통하여 http 트래픽을 서비스 쪽으로 어떻게 포워딩 하는지에 대하여 알아보았다. 그런데 HTTP 통신은 한 가지 단점이 있다.

패킷 스니핑 등 도중에 패킷을 감청했을 때 개인정보가 유출 될 수 있다는 점이다.

그렇기 때문에 TLS를 통해 통신할 수 있도록 설정함으로 써 클라이언트와 Ingress 간에 송수신되는 트래픽을 암호화해서 도중에 감청할 수 없도록 할 필요성이 있다.

암호화, TLS 등 어려운 단어가 많아서 다소 혼란 스러울 수 있겠으나 생각보다 쉬운 내용이므로 차근차근 따라가 보자.

첫번째, Key와 인증서를 생성한다.

form.

openssl genrsa -out <key-file-name>.key 2048

openssl req -new -x509 -key <key-file-name>.key -out <cert-file-name>.cert -days 360 -subj /CN=<domain-addr>

ex.

openssl genrsa -out niniz.key 2048

openssl req -new -x509 -key niniz.key -out niniz.cert -days 360 -subj /CN=niniz.com

-

두번째, Key와 인증서로 쿠버네티스 secret 생성

form.

kubectl create secret tls <secret-obj-name> --cert=<cert-file-name>.cert --key=<key-file-name>.key

ex.

kubectl create secret tls niniz-secret --cert=niniz.cert --key=niniz.key

-

세번째, 해당 Secret을 Ingress를 적용한다.

기존에 생성되어있던 Ingress를 edit 명령어로 수정한다.

kubectl edit ingress <ingress-obj-name>

-

그러면 vi 편집기로 ingress의 속성을 수정할 수 있다.

apiVersion: extensions/v1beta1
kind: Ingress
...
spec:
  rules:
  - host: niniz.com
    http:
      paths:
      - backend:
          serviceName: jordy-node-port-svc
          servicePort: 80
        path: /jordy
      - backend:
          serviceName: scappy-node-port-svc
          servicePort: 80
        path: /scappy
# tls part [start]
  tls:
  - hosts:
    - niniz.com
    secretName: niniz-secret
# tls part [end]
status:
  loadBalancer:
    ingress:
    - ip: 34.000.000.000

tls 설정을 위한 코드는 주석을 보면 알 수 있듯이 spec.tls 부분이다.
niniz.com 이란 주소에 niniz-secret이란 이름으로 된 시크릿을 적용한다는 뜻이다.

-

이미 생성된 Ingress가 있고 해당 Ingress에 속성을 추가하려면 아래 명령어를 실행하면 된다.

kubectl apply -f <file-name>.<file-extention>

-

인증서와 키를 담고 있는 secret을 테스트 하기 위해서는 아래 명령어를 실행하면 된다.

form.

curl -k -v https://<domain-addr>

ex.

curl -k -v https://niniz.com/jordy

-

curl의 옵션에 대해 설명 하겠다.

-k : '해당 사이트의 SSL 인증서에 대한 검증을 하지 않고 연결한다.' 라는 의미이다. SSL 인증서는 세계적으로 검증된 기관으로 발급 받은 후 적용하는 것이 원칙인데, 현재 우리가 적용한 인증서는 자체 발급한 인증서이기 때문에 공인인증서가 아니다.

-v : curl 이 동작하며 자세한 정보를 출력 해주는 옵션이다.

-

테스트 간에 아래와 같은 오류가 출력될 수 있다.

curl: (35) SSL connect error

-

그럴 때는 curl의 버전을 올려주면 된다.

-

Reference

  1. Service 개념 및 사진 - Kubernetes In Action(Marko Luksa)

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함