Kubernetes를 배워보자 6일차 - ReadinessProbe, LivenessProbe, NetworkPolicy

0

kubernetes

목록 보기
6/13
post-custom-banner

ReadinessProbe 구현

ReadinessProbeLivenessProbe는 end user에게 application을 제공하기 위해서는 필수와도 같은 존재이다. ReadinessProbe를 먼저 구현해보도록 하고, 이를 통해 우리의 container들이 traffic을 받아들일 준비가 되었는 지 아닌 지 보장할 수 있도록 만들어보자.

Readiness probes는 엄밀히 말하자면 service가 아니다. 그러나, service와 긴밀한 관계가 있다.

ReadinessProbe는 타겟이 되는 pod가 traffic을 받을 준비가 완전히 되었는 지 보장하기 위해 사용한다. 이를 service를 이용하여 확인하는 것이다.

왜 굳이 pod가 traffic을 받을 준비가 되었는 지 확인해야할까?? kubernetes cluster 세상에서는 pod와 service가 분리되어 존재한다. service가 만들어져 client의 요청을 받아도, pod는 죽어있는 상태거나 다시 deploy될 수 있는 상태여서 traffic을 못받을 수 있다는 것이다. 이러한 경우, client는 계속해서 응답을 받을 때까지 대기해있거나 원하던 정보를 얻지 못하고 다음 단계로 나갈 수 있다. 이는 UX 입장에서 좋지 못한 경험이다.

따라서, readiness를 확인하여 pod가 traffic을 받을 수 있는 지 아닌 지를 판단하는 것이다. 이는 추가적인 configuration을 pod에 추가하면 된다.

pod의 readiness probe가 설정되면 control plane에 해당 pod가 traffic을 받을 준비가 되었다는 signal을 보낸다. 만약, pod가 준비되어있지 않으면 service는 traffic을 전달하지 않는다.

ReadinessProbe의 구현은 pod의 yaml manifest에 추가 설정을 붙이면 된다. 즉, Service의 일종이 아니라는 것이다. pod object의 spec에 configuration을 설정함으로서 kubernetes에게 pod가 완전히 ready되도록 기다려 달라고 요청하고, 완료되면 traffic을 service를 통해 받을 수 있도록 한다.

ReadinessProbe는 서로 다른 3가지 타입이 있다.

  1. Command: pod가 ready인지 아닌 지를 판별하기 위해 command을 사용하여 exit code가 0이 나오도록 하겠다는 것이다.
  2. HTTP: HTTP request를 전달하여 200 <= response code < 400 의 응답이 오면 pod가 ready되었다고 판단한다.
  3. TCP: TCP 연결 시도를 만든다. 만약 connection이 성공하면 pod는 ready이다.

이제 yaml 파일을 만들어보도록 하자.

  • nginx-pod-with-readiness-http.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx-pod-with-readiness-http
spec:
  containers:
    - name: nginx-pod-with-readiness-http
      image: nginx
      readinessProbe:
        initialDelaySeconds: 5
        periodSeconds: 5
        httpGet:
          path: /ready
          port: 80

kindPod이다. Service가 아니다. readinenssProbe라는 키워드가 추가되었는데, 이 부분을 추가하면 자동으로 kubernetes control plane에서 httpGet.path, httpGet.port로 요청을 보낸다. 이 때 요청 주기는 처음 시작할 때 initialDelaySeconds만큼 쉬고, periodSeconds마다 보낸다. 즉, readiness가 확인되어도 주기적으로 시간이 될 때마다 계속해서 check를 한다는 것이다.

위의 ReadinessProbe pod를 만들어보도록 하자.

kubectl create -f nginx-pod-with-readiness-http.yaml 
pod/nginx-pod-with-readiness-http created

list를 확인해보면 READY0/1이다.

kubectl get po nginx-pod-with-readiness-http 
NAME                            READY   STATUS    RESTARTS   AGE
nginx-pod-with-readiness-http   0/1     Running   0          16s

ReadinessProbe가 구동되고 있지만, 80포트로 /ready의 요청이 정상적인 응답으로 오지 않는다는 것이다.

kubectl logs로 확인해보면 계속해서 control plane에서 /ready 요청을 보내는 것을 알 수 있다.

그럼, /ready를 라우팅하는 웹 어플리케이션 서버를 하나 만들어서 ReadinessProbe가 성공하는 지 확인해보도록 하자.

  • test_server.go
package main

import (
	"fmt"
	"net/http"
)

func readyHandler(res http.ResponseWriter, req *http.Request) {
	fmt.Println("Get ready check")
	res.Write([]byte("Success to Ready!"))
}

func healthHandler(res http.ResponseWriter, req *http.Request) {
	fmt.Println("Get health check")
	res.Write([]byte("Success to health!"))
}

func main() {
	app := http.NewServeMux()
	app.HandleFunc("/ready", readyHandler)
	app.HandleFunc("/health", healthHandler)
	http.ListenAndServe(":80", app)
}

다음은 80 port로 서버를 열고 /ready로 라우팅을 전달하는 간단한 golang 어플리케이션 서버이다. go.mod까지 만들어주도록 하자.

go mod init test-server

go.mod가 만들어졌을 것이다.

다음의 Dockerfile을 사용하여 test_server.go 파일을 빌드하도록 하자.

  • Dockerfile
FROM golang:1.19

WORKDIR app

COPY go.mod ./
COPY *.go ./
RUN go build -o /test_server
EXPOSE 80

CMD ["/test_server"]

다음으로 docker image를 빌드해보도록 하자.

docker build --tag test-server .

다음의 명령어로 빌드 후에는 test-server:latest docker 이미지가 만들어졌을 것이다. 이를 사용하여 pod를 만들어보도록 하자.

  • test-server-readiness.yaml
apiVersion: v1
kind: Pod
metadata:
  name: test-server-readiness-http
spec:
  containers:
    - name: test-server
      image: test-server:latest
      imagePullPolicy: IfNotPresent
      readinessProbe:
        initialDelaySeconds: 5
        periodSeconds: 5
        httpGet:
          path: /ready
          port: 80

test-server:latest를 image로 하는 pod를 만들 수 있다. imagePullPolicyIfNotPresent인데, kubernetes는 기본적으로 docker image를 가져올 때 docker hub에서부터 가져온다. 만약 hub에서 안가져오고 local의 image를 쓰고 싶다면 IfNotPresent 옵션을 주어 local에 없으면 docker hub로 부터 가져오도록 하는 것이다.

이제 pod를 만들어보도록 하자.

kubectl create -f ./test-server-readiness.yaml
pod/test-server-readiness-http created

다음으로 ready에 성공했는 지 확인하도록 하자.

kubectl get pod test-server-readiness-http 
NAME                         READY   STATUS    RESTARTS   AGE
test-server-readiness-http   1/1     Running   0          28s

READY에 성공한 것을 확인할 수 있다.

로그를 확인하면 다음과 같다.

kubectl logs test-server-readiness-http -f
Get ready check
Get ready check
Get ready check
Get ready check
Get ready check
...

계속해서 ReadinessProbe가 이루어지고 있음을 알 수 있다.

ReadinessProbe는 굉장히 중요하다. 해당 application이 확실 traffic을 받을 수 있는 상태인지 확인할 수 있기 때문이다.

그런데, ReadinessProbe의 한 가지 문제가 있는데, 바로 endpoint을 받을 준비가 되었지만 pod가 제정신이 아닌 경우가 있다. 즉, 정상적으로 동작하지 않는 경우가 있다는 것이다. 이를 위해 설정하는 것이 바로 LivenessProbe이다.

LivenessProbe가 무엇이고 왜 쓰는가?

LivenessProbe를 통해서 health check를 할 수 있다. 이는 pod가 broken state인지 아닌지 를 결정하는 것으로, 오랫동안 살아있어야 하는 process에 많이 사용된다. 가령 하나의 service에 3개의 pod들이 붙어있다고 생각해보자. pod 중 하나가 broken state에 빠졌지만, readiness check은 true일 수 있다.

[사진5]

이런 경우 pod의 healthy가 정상이 아니기 때문에 해당 pod에 request를 전달하면 안된다. 그러나 readiness만 확인하고 service가 traffic을 흘려보내면 end user는 좋지 않은 경험을 하게되고, 전체 프로그램에 큰 문제가 야기될 수 있다.

때문에, pod가 borken 상태인지 아닌지 확인하는 check가 필요하고 해당 pod를 kill시켜 다시 시작시킬 필요가 있다.

LivenessProbe가 이러한 문제의 해결책이며, 이는 pod level에서 만들어진다. 단, 주의할 것은 LivenessProbe는 pod를 고치는 것이 아니라 단순히 비정상적인 pod를 탐지하고 죽이는 명령어를 반복할 뿐이다. 즉, 그냥 처음부터 잘못된 pod는 계속해서 죽고 살고를 반복할 뿐이지 문제가 해결되지 않는다는 것이다.

LivenessProbe 구현

LivenessProbe는 healthcheck로 긴 시간동안 주기적으로 application의 상태를 확인하고 추적한다. 즉, 한 번만 check가 확인된다해서 끝나는 것이 아니라, 계속해서 실행된다.

다음의 3가지 type이 존재한다.

  1. Command: command를 실행하여 container의 상태를 판단한다. exit code가 0이 되어야 healthy이다.
  2. HTTP: pod에 대한 HTTP request를 실행하고, 그 결과가 200 ~ 400 사이의 응답(단, 200은 포함, 400은 미포함)일 경우 healthy하다.
  3. TCP: tcp 연결을 정의하여 connection이 성공하면 pod는 healthy하다.

LivenessProbe를 구현할 때 조심해야할 것은 periodSecondsinitialDelaySeconds를 잘 써줘야 한다는 것이다. periodSeconds는 주기적으로 healthy체크를 하는 부분이고, initialDelaySeconds는 초기에 얼마만큼 기다릴 것인지를 의미한다. initialDelaySeconds가 너무 짧으면 준비가 되지 않은 상태에서 요청을 받아 healthy하지 않다고 판단을 내릴 수 있다.

이전에 만들었던 test_server.go 파일을 통해서 livenessProbe를 해보도록 하자.

  • test-server-liveness-http.yaml
apiVersion: v1
kind: Pod
metadata:
  name: test-server-liveness-http
spec:
  containers:
    - name: test-server
      image: test-server:latest
      imagePullPolicy: IfNotPresent
      livenessProbe:
        initialDelaySeconds: 5
        periodSeconds: 5
        httpGet:
          path: /health
          port: 80
          httpHeaders:
            - name: My-Custom-Header
              value: My-Coustom-Header

딱히 추가된 것은 없다. livenessProbe로 바뀐 것이 다이다. path/health로 두었다.

kubectl create -f  test-server-liveness-http.yaml 
pod/test-server-liveness-http created

생성 후에 로그를 확인하여 /health요청이 제대로 오고 있는 지 확인하도록 하자.

kubectl logs test-server-liveness-http -f
Get health check
Get health check
Get health check
Get health check
...

Get health check이 계속 올 것이다. 이처럼 healthcheck는 계속해서 주기적으로 설정한 시간 동안 LivenessProbe를 한다.

ReadinessProbe와 LivenessProbe 함께 쓰기

같은 pod에 ReadinessProbeLivenessProbe를 함께 사용할 수 있다. 이들은 서로 다른 목적을 가지고 있으며, 한 가지가 Okay인 상태여도 다른 한 가지가 아닐 수 있다.

이들은 거의 같은 방식으로 설정되며 위에 우리가 했던 실습과 별반 차이가 없다. 다음의 parameter들을 설정 시에 둘 다 쓰이는 파라미터들이다.

  1. initialDelaySeconds: 첫번째 probe 실행 이전에 기다려야할 시간
  2. periodSeconds: 다음 probe 주기
  3. timeoutSeconds: timeout 이전에 기다려야할 시간
  4. successThreshold: pod가 ready 또는 healthy인지 판단하기위한 성공횟수
  5. failureThreshold: pod가 not-ready 또는 unhealthy인지 판단하기위한 실패횟수

위에서 만든 test-server를 이용하여 readiness와 liveness를 같이 check해보도록 하자.

  • test-server-readiness-liveness.yaml
apiVersion: v1
kind: Pod
metadata:
  name: test-server-liveness-http
spec:
  containers:
    - name: test-server
      image: test-server:latest
      imagePullPolicy: IfNotPresent
      livenessProbe:
        initialDelaySeconds: 5
        periodSeconds: 5
        httpGet:
          path: /health
          port: 80
          httpHeaders:
            - name: My-Custom-Header
              value: My-Coustom-Header
      readinessProbe:
        initialDelaySeconds: 5
        periodSeconds: 5
        httpGet:
          path: /ready
          port: 80

다음과 같이 livenessProbereadinessProbe 두 개를 하나의 yaml 파일에 나누어 빌드를 하면 된다.

실행시켜보고 로그를 확인헤보자.

kubectl create -f test-server-readiness-liveness.yaml 
pod/test-server-liveness-http created

...

kubectl logs test-server-liveness-http -f
Get health check
Get ready check
Get health check
Get ready check
Get health check
Get ready check
Get health check
Get ready check

로그를 확인해보면 계속해서 ReadinessProbeLivenessProbe가 구동되고 있는 것을 볼 수 있다. 이와 같이 이들을 같이 사용하는 것은 문제가 없다.

NetworkPolicy object를 사용하여 pod를 보호하기

NetwrokPolicy object는 network firewalls(방화벽)을 cluster에 직접 구현해준다.

kubernetes cluster내의 micro service들은 서로 간의 network 통신으로 의사소통을 할 수 있다. 이 과정에서 특정 pod에 대한 방화벽을 만들어 보안을 집중시키고 싶을 수 있다.

keubernetes에서는 NetworkPolicy라는 방화벽을 구현하였다. 이는 새로운 resource 종류로 오직 허용된 IP만 받고 그렇지 않은 IP에 대해서는 요청을 제어할 수 있다.

NetworkPolicy를 사용하는 이점은 다음과 같다.
1. egress/ingress rule을 Classless Inter-Domain Routing(CIDR) blocks에 기반으로 만들 수 있다.
2. egress/ingress rule을 pod의 label과 selector를 기반으로 만들 수 있다.
3. egress/ingress rule을 namespace에 근거하여 만들 수 있다.

NetworkPolicy가 동작하기 위해서는 kubernetes cluster에 CNI plugin을 설치해야한다. CNI plugin은 일반적으로 kubernetes에 default로 설치되지 않는다. 만약 minikube를 사용하고 있다면 Calico가 기본적으로 설치되어 있을텐데, 일반 kubernetes cluster에도 Calico를 설치하면 NetworkPolicy를 지원할 수 있다.

minikube에서는 다음과 같은 명령어로 가능하다.

minikube start --network-plugin=cni --cni=calico

만약 cloud에서 kubernetes를 사용하고 있다면 cloud 공급자가 제공하는 CNI를 사용하는 것이 좋다.

labels와 selectors를 사용하여 NetworkPolicy를 설정하기

필자는 local kubernetes cluster에 calio를 설치하여 CNI를 설정하였다.

예제로 두 개의 nginx pod를 만들도록 하자. 하나는 app=nginx-1 label을 가지고 하나는 app=nginx-2 label을 가지도록 한다.

kubectl run nginx-1 --image nginx --labels 'app=nginx-1'
pod/nginx-1 created
kubectl run nginx-2 --image nginx --labels 'app=nginx-2'
pod/nginx-2 created

kubectl get pods -o wide
NAME                        READY   STATUS    RESTARTS   AGE   IP               NODE                     NOMINATED NODE   READINESS GATES
nginx-1                     1/1     Running   0          26s   192.168.33.195   gy95park-400tca-400sca   <none>           <none>
nginx-2                     1/1     Running   0          19s   192.168.33.229   gy95park-400tca-400sca   <none>           <none>

kubectl get pods -o wide로 각 pod의 IP를 알 수 있다. nginx-1에서 nginx-2curl을 전송해보도록 하자.

kubectl exec nginx-1 -- curl 192.168.33.229
...
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>
...

요청이 잘 전송되었다.

이제 NetworkPolicy를 만들어 nginx-1에서 nginx-2로 network 요청을 보내는 것만 허용하고, 나머지는 막도록 해보자.

  • nginx-2-networkpolicy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: nginx-2-networkpolicy
spec:
  podSelector:
    matchLabels:
      app: nginx-2 # applies to witch pod
  policyTypes:
    - Ingress
  ingress:
    - from:
      - podSelector:
          matchLabels:
            app: nginx-1 # allows calls from which pod
      ports:
        - protocol: TCP
          port: 80

이제 실행해보도록 하자.

kubectl create -f nginx-2-networkpolicy.yaml 
networkpolicy.networking.k8s.io/nginx-2-networkpolicy created

kubectl get networkpolicy
NAME                    POD-SELECTOR   AGE
nginx-2-networkpolicy   app=nginx-2    10s

잘 만들어진 것을 볼 수 있다.

이제 아까와 같이 nginx-1 pod에서 nginx-2의 80포트로 요청을 보내보도록 하자.

kubectl exec nginx-1 -- curl 192.168.33.229
...
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>
...

성공적으로 보내지는 것을 볼 수 있다.

만약, 다른 port로 요청하면 어떻게 될까? nginx-container80 port 이외에는 열려있지 않기 때문에 다른 port로 요청시에 connection refused와 같이 연결에 실패해야한다. 8080 port로 요청을 보내보도록 하자.

kubectl exec nginx-1 -- curl 192.168.33.229:8080
...

계속 무한 대기하게되는 것을 볼 수 있다. 이는 NetworkPolicy에서 막았기 때문이다. 즉, nginx-1입장에서 nginx-28080으로 요청을 보냈지만 요청이 막혀서 nginx-1에게 못닫는 것이다. 따라서, connection refused와 같은 응답도 오지 않고 계속해서 기다리고만 있는 것이다.

그럼 NetworkPolicyport8080으로 변경해보고 다시 요청해보도록 하자.

  • nginx-2-networkpolicy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: nginx-2-networkpolicy
spec:
  podSelector:
    matchLabels:
      app: nginx-2 # applies to witch pod
  policyTypes:
    - Ingress
  ingress:
    - from:
      - podSelector:
          matchLabels:
            app: nginx-1 # allows calls from which pod
      ports:
        - protocol: TCP
          port: 8080

manifest 파일을 변경하였다면 kubectl apply로 적용할 수 있다.

kubectl apply -f nginx-2-networkpolicy.yaml 
Warning: kubectl apply should be used on resource created by either kubectl create --save-config or kubectl apply
networkpolicy.networking.k8s.io/nginx-2-networkpolicy configured

이제 nginx-1에서 nginx-28080 포트로 요청을 보내보도록 하자.

kubectl exec nginx-1 -- curl 192.168.33.229:8080
...
curl: (7) Failed to connect to 192.168.33.229 port 8080: Connection refused
command terminated with exit code 7

Connection refused가 발생하였다. 이는 nginx-2에서 8080포트를 열지 않았기 때문에 해당 port로 오는 request를 거절한 것이다. 즉, 이전과 달리 nginx-1에서 nginx-28080포트로 요청이 전달되었다는 것이다.

반면, 이전에 응답을 받았던 80포트로 요청을 보내면 이야기가 달라진다.

kubectl exec nginx-1 -- curl 192.168.33.229:80
...

무한 대기를 하게된다. 이는 방화벽 기능을 하고 있는 NetworkPolicy에 의해 요청이 막혔기 때문이다.

NetworkPolicy는 pod간의 보안을 위해 굉장히 효율적인 방화벽 역할을 해주므로 사용하는 것이 좋다. 또한, 일반적으로 방화벽을 설정할 때 CIDR을 기반으로 많이 사용한다. 이는 pod가 외부의 cluster로부터 호출될 때 요청을 제어할 수 있어 좋다. 또는, pod간의 통신을 제어할 때는 label selector나 namespace를 사용하는 것이 좋다.

그런데, namespace가 무엇인가?? 그건 다음 chapter에서 배워보도록 하자.

post-custom-banner

0개의 댓글