반응형

이전 글에서는 Kubernetes Cluster 상에서 App 을 Scratch 방식으로 Blue/Green 배포를 하였다. 이번에는 Argo Rollout 을 사용한 Blue/Green 배포하는 방식을 살표보자.

Nginx 혹은 AWS ALB 를 직접 연결하여 사용할 수 있지만, Blue/Green 배포는 Traffic Shifting 이 필요하지 않으므로 AWS LB → Ingress Controller 를 연결한 상태를 만들어 놓고 배포하는 방식을 설명한다.

1. Argo Rollout 설치

helm chart 를 이용하여 argo rollout 을 설치한다.

argo rollout dashboard 를 포햄하여 설치하고 싶으면 dashboard.enabled=true 를 추가하면 된다.

$ helm repo add argo https://argoproj.github.io/argo-helm
$ helm repo update

$ helm search repo argo/argo-rollouts -l
NAME                    CHART VERSION   APP VERSION     DESCRIPTION
argo/argo-rollouts      2.22.2          v1.4.0          A Helm chart for Argo Rollouts
argo/argo-rollouts      2.22.1          v1.4.0          A Helm chart for Argo Rollouts

$ helm upgrade -i argo-rollout argo/argo-rollouts --version 2.22.2 -n argo --set dashboard.enabled=true --create-namespace

argo rollout dashboard 는 인증 체계가 없다. 그러므로 포트 포워딩으로 dashboard 에 접속 하는 것을 추천한다.

$ kubectl port-forward service/argo-rollouts-dashboard 31000:3100

2. Argo Rollout kubectl plugin 설치

kubectl 로 cli 호출이 가능하도록 plugin 을 설치한다.

$ curl -LO https://github.com/argoproj/argo-rollouts/releases/download/v1.4.0/kubectl-argo-rollouts-linux-amd64
$ chmod +x kubectl-argo-rollouts-linux-amd64
$ sudo mv kubectl-argo-rollouts-linux-amd64 /usr/local/bin/kubectl-argo-rollouts

$ kubectl argo rollouts version
--- output ---
kubectl-argo-rollouts: v1.4.0+e40c9fe
  BuildDate: 2023-01-09T20:20:38Z
  GitCommit: e40c9fe8a2f7fee9d8ee1c56b4c6c7b983fce135
  GitTreeState: clean
  GoVersion: go1.19.4
  Compiler: gc
  Platform: linux/amd64

argo rollout bash complete 도 설치한다.

$ kubectl argo rollouts completion bash | tee /home/ubuntu/.kube/kubectl-argo-rollouts > /dev/null

$ vi ~/.bash_profile
source '/home/ubuntu/.kube/completion.bash.inc'
source '/home/ubuntu/.kube/kubectl-argo-rollouts'

PATH=/home/ubuntu/bin:$PATH

if [ -f ~/.bashrc ]; then
    . ~/.bashrc
fi

3. 최초 App 배포 (Blue Deployment)

초기 app (blue)을 배포한다. 이전 글에서 사용된 seungkyua/nginx:blue 이미지를 배포한다. 단 replicas 를 0 으로 배포한다. 이렇게 배포하면 실제 pod 는 실행되지 않지만 pod template 은 배포된 상태가 된다. pod template 은 나중에 rollout 에서 참조하여 사용한다.

$ cat nginx-blue-deploy.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: nginx-blue-green
  name: nginx-blue-green
spec:
  replicas: 0
  selector:
    matchLabels:
      app: nginx-blue-green
      version: blue-green
  template:
    metadata:
      labels:
        app: nginx-blue-green
        version: blue-green
    spec:
      containers:
      - image: seungkyua/nginx:blue
        name: nginx

$ kubectl apply -f nginx-blue-deploy.yaml

$ kubectl get deploy,pod
NAME                               READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/nginx-blue-green   0/0     0            0           11s

Service 를 배포한다.

$ cat nginx-blue-green-svc.yaml
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: nginx-blue-green
  name: nginx-blue-green-svc
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: nginx-blue-green
    version: blue-green
  type: ClusterIP

$ kubectl apply -f nginx-blue-green-svc.yaml

그리고 blue deployment app 에 웹접속이 가능하게 ingress 를 배포한다. ingress 를 배포하더라도 아직 웹 접속은 불가능하다. 앞에서 deployment 의 replicas 를 0 으로 생성했기 때문에 실행되고 있는 pod 가 없기 때문이다. (나중에 접속을 위해서 /etc/hostsnginx-blue-green.taco-cat.xyz 를 등록해 놓자)

$ cat nginx-blue-green-ingress.yaml

---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: nginx-blue-green-ingress
spec:
  ingressClassName: nginx
  rules:
  - host: nginx-blue-green.taco-cat.xyz
    http:
      paths:
      - pathType: ImplementationSpecific
        backend:
          service:
            name: nginx-blue-green-svc
            port:
              number: 80

$ kubectl apply -f nginx-blue-green-ingress.yaml

Rollout 커스텀 리소스 배포

이제 초기 환경으로 Rollout 을 배포한다. Rollout 을 배포하면 pod 가 생성된다. workloadRef 영역은 Deployment 에서 Pod Template 영역과 일치한다. 그래서 이미 배포된 Deployment 를 참조하게 정의했다.

마지막 라인의 autoPromotionEnabledfalse 로 하여 수동으로 Blue/Green 을 확인하면서 배포를 할 수 있게 한다.

$ cat nginx-rollout.yaml

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: nginx-rollout
spec:
  replicas: 2
  revisionHistoryLimit: 5
  selector:
    matchLabels:
      app: nginx-blue-green
  workloadRef:
    apiVersion: apps/v1
    kind: Deployment
    name: nginx-blue-green
  strategy:
    blueGreen:
      activeService: nginx-blue-green-svc
      autoPromotionEnabled: false

$ kubectl apply -f nginx-rollout.yaml

이제 배포가 완료되면 아래와 같이 리소스가 생성되었음을 확인할 수 있다. rollout pod 와 rollout 에서 사용하는 ReplicaSet 이 생성되어 있음을 알 수 있다.

$ kubectl argo rollouts list rollout
NAME           STRATEGY   STATUS        STEP  SET-WEIGHT  READY  DESIRED  UP-TO-DATE  AVAILABLE
nginx-rollout  BlueGreen  Healthy       -     -           2/2    2        2           2

$ kubectl get pods,deploy,rs
NAME                                 READY   STATUS    RESTARTS   AGE
pod/nginx-rollout-85c4bfb654-jmts7   1/1     Running   0          2m29s
pod/nginx-rollout-85c4bfb654-s46sm   1/1     Running   0          2m29s

NAME                               READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/nginx-blue-green   0/0     0            0           4m50s

NAME                                         DESIRED   CURRENT   READY   AGE
replicaset.apps/nginx-blue-green-8b4f9cddb   0         0         0       4m50s
replicaset.apps/nginx-rollout-85c4bfb654     2         2         2       2m29s

웹으로 접속하면 아래와 같은 화면을 볼 수 있다.

혹은 curl 로도 확인할 수 있다.

$ curl nginx-blue-green.taco-cat.xyz

--- output ---
<!DOCTYPE html>
<html>
<body style="background-color:blue;">
<h1>This is a blue webserver</h1>
</body>
</html>

Rollout dashboard 에는 아래와 같이 나온다.

4. App 업그레이드 배포 (Green Deployment)

app 을 수정하여 배포해 보자. app 은 Deployment 를 수정해서 배포하면 된다.

$ cat nginx-green-deploy.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: nginx-blue-green
  name: nginx-blue-green
spec:
  replicas: 0
  selector:
    matchLabels:
      app: nginx-blue-green
      version: blue-green
  template:
    metadata:
      labels:
        app: nginx-blue-green
        version: blue-green
    spec:
      containers:
      - image: seungkyua/nginx:green
        name: nginx

$ kubectl apply -f nginx-green-deploy.yaml

Deployment 가 업그레이드 되어 배포했기 때문에 Rollout 이 이를 인식하고 Green 에 해당하는 추가 Pod 와 ReplicaSet 을 생성한다. 그리고 Rollout 리소스의 상태는 Paused 가 된. (앞에서 Rollout 리소스 배포 시에 autoPromotionEnabledfalse 로 하였기 때문이다)

$ kubectl argo rollouts list rollout
NAME           STRATEGY   STATUS        STEP  SET-WEIGHT  READY  DESIRED  UP-TO-DATE  AVAILABLE
nginx-rollout  BlueGreen  Paused        -     -           2/4    2        2           2

Pod 와 ReplicaSet 을 조회해 보면 아래와 같다. Blue 해당하는 ReplicaSet 1 개, Pod 2개, Green 에 해당하는 ReplicaSet 1개 Pod 2 개가 떠 있는 것을 알 수 있다.

Rollout Dashboar 는 아래와 같은 Pause 상태이다.

5. Rollout 진행 완료 (promote)

완전히 Green 으로 변경하려면 Rollout 을 promote 하여 최종 적용을 하던지 abort 하여 중단, 혹은 undo 하여 Pause 보다 이전 단계이 최초 Blue app 배포 단계로 돌아가는 방법이 있다.

Green 으로 진행하는 promote 를 해보자.

$ kubectl argo rollouts promote nginx-rollout

--- output ---
rollout 'nginx-rollout' promoted

ReplicaSet 은 남아 있지만 Pod 는 Green 으로 배포된 것만 남아있는 것을 확인할 수 있다.

$ kubectl get pods,rs -l rollouts-pod-template-hash --show-labels
NAME                                 READY   STATUS    RESTARTS   AGE   LABELS
pod/nginx-rollout-569b8595bf-8s94v   1/1     Running   0          10m   app=nginx-blue-green,rollouts-pod-template-hash=569b8595bf,version=blue-green
pod/nginx-rollout-569b8595bf-c7fgg   1/1     Running   0          10m   app=nginx-blue-green,rollouts-pod-template-hash=569b8595bf,version=blue-green

NAME                                       DESIRED   CURRENT   READY   AGE   LABELS
replicaset.apps/nginx-rollout-569b8595bf   2         2         2       10m   app=nginx-blue-green,rollouts-pod-template-hash=569b8595bf,version=blue-green
replicaset.apps/nginx-rollout-85c4bfb654   0         0         0       19m   app=nginx-blue-green,rollouts-pod-template-hash=85c4bfb654,version=blue-green

Rollout Dashboard 에서도 완료된 것을 알 수 있다.

마지막으로 웹 화면으로 확인한다.

마치며…

Argo Rollout 은 Deployment 변경에서만 인식을 한다. ConfigMap 이나 Secret 과 같은 다른 리소스는 지원하지 않으니 Rollout 에서 이를 지원하는 방법은 추가로 고민해야 한다.

반응형
Posted by seungkyua@gmail.com
,
반응형

TACO 에서는 Kubernetes 에 워크로드를 배포하기 위해서 Decapod 라는 자체 빌드 및 배포 체계를 갖고 있다. Decapod 는 Helm Chart 의 value override 기능과 Kustomize 의 plugin 기능을 개발하여 적용한 또 다른 value override 를 모두 사용하고 있다.

Helm Chart 와 Kustomize 모두 value oeverride 기능을 모두 제공하는데 왜 2가지를 모두 사용할까? 그 이유는 Helm Chart 는 이미 다양하게 제공되고 있는 것들이 많아 가져다 쓰면 되고, value 값들을 하나의 yaml 파일로 합쳐서 관리하기 위해서 Kustomize 의 plugin 을 개발하여 사용하고 있다.

즉, Decapod 체계는 다음과 같은 장점이 있다.

  1. Helm Chart 기반으로 default custom value 값을 지정할 수 있다. (decapod-base-yaml)
  2. Kustomize plugin 을 개발하여 각 사이트마다 갖는 여러 helm chart 의 고유 value 값들을 1개의 yaml 파일에 합쳐서 관리할 수 있다.

하지만 Helm Chart 를 제공하지 않는 app 들은 어떻게 지원할까? 예를 들어 Kubeflow 의 경우 Helm Chart 를 제공하지 않지만 Kustomize 를 제공하고 있으니 이를 지원하는 방법도 필요해 보인다.

Kustomize 활용을 위한 기본 디렉토리 (base repo)

kustomize 는 설치되어 있다고 가정하고 바로 활용을 위한 기본 디렉토리를 살펴보자.

$ tree
.
├── LICENSE
├── README.md
└── service-mesh
    └── nginx
        ├── aws-msa-reference
        │   ├── kustomization.yaml
        │   └── site-values.yaml
        └── base
            ├── kustomization.yaml
            ├── nginx-deployment.yaml
            ├── nginx-service.yaml
            └── site-values.yaml

소스 홈 디렉토리 아래에는 service-mesh 라는 서비스 디렉토리가 있다. 여기에는 nginx, istio, jaeger, kaili 등 다양한 application 이 동시에 설치되어야 하는데 각 app 을 나타내는 디렉토리 (여기서는 편의상 nginx 만 설명한다)가 존재한다.

nginx 설치를 위해서는 보통 nginx-deployment.yaml 과 nginx-service.yaml 이 필요하며 이를 base 디렉토리에 위치시킨다.

$ cat nginx-deployment.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: nginx-blue
  name: nginx-blue
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx-blue
      version: blue
  template:
    metadata:
      labels:
        app: nginx-blue
        version: blue
    spec:
      containers:
      - image: seungkyua/nginx:blue
        name: nginx 
$ cat nginx-service.yaml
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-blue-svc
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: nginx-blue
    version: blue
  type: LoadBalancer

여기까지는 Helm chart 와 동일하다. 다만 chart 는 template 을 활용하여 value 값을 override 할 수 있는데 반하여 kustomize 는 kustomize.yaml 을 통해서 value 값을 override 할 수 있다.

kustomize.yaml 을 지정하고 기본적으로 업데이트할 디폴트 값을 site-values.yaml 이라는 파일에 지정한다.

resources 는 kubernetes 에 설치할 리소스들에 대한 yaml 리스트이고 patchesStrategicMerge 는 kustomize 에서 제공하는 yaml 합성 기능 중에 하나의 방법이다.

아래의 경우에는 site-values.yaml 값과 nginx-deployment.yaml, nginx-service.yaml 값을 합쳐서 만든다.

중복되는 경우에는 site-values.yaml 값을 우선시 한다.

$ cat kustomization.yaml
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- nginx-deployment.yaml
- nginx-service.yaml

patchesStrategicMerge:
- site-values.yaml

site-values.yaml 에는 override 할 default value 값을 가진다.

deployment 는 replicas 값을, service 는 NodePort 타입과 nodePort 값을 가진다. (이것을 base 값이라 생각하면 이해하기 쉽다. nginx helm chart 는 기본값이 replicas 1 인데 우리는 기본 값을 replicas 2 로 의도한 것이다.)

$ cat site-values.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-blue
spec:
  replicas: 2

---
apiVersion: v1
kind: Service
metadata:
  name: nginx-blue-svc
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
    nodePort: 32080
  type: NodePort

여기서 잠깐. nginx-deployment.yaml 과 nginx-service.yaml 값을 처음부터 바꿔서 넣으면 site-values.yaml 이 필요하지 않을 텐데 왜 굳이 이걸 만들지?

그건 kustomize 혹은 helm chart 를 제공하기 때문에 가져다 쓰는 경우도 많은데 원래의 작성 값들을 바꿔서 관리하지 않으려고 하는 의도이다. 업스트림에서 만들어 진 것이 있으면 원본은 그대로 가져다 쓰는 것이 추후 유지 보수 관점에서 편리하기 때문이다.

이제 여기까지 만든 내용을 kustomize 로 build 해 보자.

$ cd service-mesh/nginx

$ kustomize build base

--- output ---
apiVersion: v1
kind: Service
metadata:
  name: nginx-blue-svc
spec:
  ports:
  - nodePort: 32080
    port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: nginx-blue
    version: blue
  type: NodePort
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: nginx-blue
  name: nginx-blue
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx-blue
      version: blue
  template:
    metadata:
      labels:
        app: nginx-blue
        version: blue
    spec:
      containers:
      - image: seungkyua/nginx:blue
        name: nginx

Site 별 디렉토리 (Site repo)

이제 특정한 사이트에 다른 값으로 설치해야 한다고 가정해 보자.

aws-msa-reference 라는 신규 사이트 디렉토리를 만들고 kustomize.yaml 과 value 값을 모아둔 site-values.yaml 을 만든다.

$ tree
.
├── LICENSE
├── README.md
└── service-mesh
    └── nginx
        ├── aws-msa-reference
        │   ├── kustomization.yaml
        │   └── site-values.yaml
        └── base
            ├── kustomization.yaml
            ├── nginx-deployment.yaml
            ├── nginx-service.yaml
            └── site-values.yaml

지금은 service-mesh/nginx 디렉토리 아리에 aws-msa-reference 가 있지만 이 디렉토리는 다른 repo 에서 관리하다가 kustomize build 를 하려할 때 해당 디렉토리로 복사해 오는 방법을 쓸 수 있다. (decapod 에서는 실제로 decapod-site 라는 repo 에 따로 사이트 값들을 관리하고 build 할 때 복사하는 방식을 사용하고 있다)

kustomize.yaml 과 site-values.yaml 을 보자.

$ cat kustomization.yaml
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
bases:
- ../base

patchesStrategicMerge:
- site-values.yaml
$ cat site-values.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-blue
spec:
  replicas: 3

---
apiVersion: v1
kind: Service
metadata:
  name: nginx-blue-svc
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  type: LoadBalancer

새로운 신규 사이트에는 replicas 를 3으로 Service Type 을 LoadBalancer 로 설치하고자 한다.

이를 build 하면 다음과 같다.

$ kustomize build aws-msa-reference

--- output ---
apiVersion: v1
kind: Service
metadata:
  name: nginx-blue-svc
spec:
  ports:
  - nodePort: 32080
    port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: nginx-blue
    version: blue
  type: LoadBalancer
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: nginx-blue
  name: nginx-blue
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx-blue
      version: blue
  template:
    metadata:
      labels:
        app: nginx-blue
        version: blue
    spec:
      containers:
      - image: seungkyua/nginx:blue
        name: nginx

결과가 site 별로 지정한 값으로 잘 변경되었다.

어디에 적용할까?

kustomize 로 빌드된 결과 yaml 들을 특정 repo 에 저장하고 Argo CD 와 같은 tool 을 적용하면 효과적인 GitOps 체계를 만들 수 있다.

Tekton pipeline 이나 Argo workflow, Jenkins pipeline 으로 CI 를 구축하고 Argo CD 로 CD 를 연결하면 GitOps CICD 를 구축할 수 있다. (이것까지 글로 써볼까? ㅎ)

반응형
Posted by seungkyua@gmail.com
,
반응형

Kubernetes 에서 Blue/Green 배포하는 방법을 알아보자.

  1. Blue 와 Green 버전의 Container 이미지 만들기
  2. Blue 버전의 Deployment 와 LoadBalancer 타입의 Service 배포
  3. Green 버전의 Deployment 와 NodePort 타입의 Service 배포
  4. Patch 로 Service EndPoint 변경

1. Blue 와 Green 버전의 Container 이미지 만들기

Container 이미지는 nginx 를 상속받아 쉽게 만들 수 있다. nginx 가 바라보는 web root 는 /usr/share/nginx/html 이므로 여기에 blue 버전을 표시할 수 있는 html 을 넣어준다.

index-blue.html

<!DOCTYPE html>
<html>
<body style="background-color:blue;">
<h1>This is a blue webserver</h1>
</body>
</html>

index-blue.htmlindex.html 로 변경하여 복사한다.

Dockerfile-blue

FROM nginx
COPY index-blue.html /usr/share/nginx/html/index.html

seungkyua/nginx:blue 이름과 태그를 갖는 Container 이미지를 만든다.

$ docker build -t seungkyua/nginx:blue -f Dockerfile-blue . 

이미지가 정확한지 test 해본다.

$ docker run --rm -d -p 8080:80 --name nginx-blue seungkyua/nginx:blue

$ curl localhost:8080
<!DOCTYPE html>
<html>
<body style="background-color:blue;">
<h1>This is a blue webserver</h1>
</body>
</html>

이미지가 잘 확인되었으니 docker hub 에 push 한다.

$ docker login -u seungkyua
password: 

$ docker push seungkyua/nginx:blue

같은 방식으로 Green 이미지를 만들어서 docker hub 에 push 한다.

index-green.html

<!DOCTYPE html>
<html>
<body style="background-color:green;">
<h1>This is a green webserver</h1>
</body>
</html>

Dockerfile-green

$ cat Dockerfile-green
FROM nginx
COPY index-green.html /usr/share/nginx/html/index.html
$ docker build -t seungkyua/nginx:green -f Dockerfile-green .
$ docker push seungkyua/nginx:green

2. Blue 버전의 Deployment 와 LoadBalancer 타입의 Service 배포

이미지가 준비되었으니 Blue 버전의 deployment 와 service yaml 을 만들어 배포한다.

nginx-blue-deploy.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: nginx-blue
  name: nginx-blue
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx-blue
      version: blue
  template:
    metadata:
      labels:
        app: nginx-blue
        version: blue
    spec:
      containers:
      - image: seungkyua/nginx:blue
        name: nginx
$ kubectl apply -f nginx-blue-deploy.yaml

Service 를 배포할 때는 Blue pod 의 label 을 selector 로 지정해 줘야 한다.

nginx-blue-green-svc.yaml

apiVersion: v1
kind: Service
metadata:
  labels:
    app: nginx-blue-green
  name: nginx-blue-green-svc
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: nginx-blue
    version: blue
  type: LoadBalancer
$ kubectl apply -f nginx-blue-green-svc.yaml

blue version 은 웹 브라우저에서 접속하기 위해서 Service 를 LoadBalancer 타입으로 생성하였다.

$ kubectl get svc
NAME                   TYPE           CLUSTER-IP      EXTERNAL-IP                                                                  PORT(S)        AGE
kubernetes             ClusterIP      10.233.0.1      <none>                                                                       443/TCP        12d
nginx-blue-green-svc   LoadBalancer   10.233.30.78    aa1d4e1994e454eb5aea607cfdfd3dcf-23761053.ap-northeast-2.elb.amazonaws.com   80:32234/TCP   5m18s

웹브라우저에 접속하면 아래와 같이 blue 버전으로 접속이 된다.

3. Green 버전의 Deployment 와 NodePort 타입의 Service 배포

이제 새로운 버전인 Green 버전을 배포해 보자. image 와 label 을 잘 확인해야 한다.

nginx-green-deploy.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: nginx-green
  name: nginx-green
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx-green
      version: green
  template:
    metadata:
      labels:
        app: nginx-green
        version: green
    spec:
      containers:
      - image: seungkyua/nginx:green
        name: nginx
$ kubectl apply -f nginx-green-deploy.yaml

Service 는 Green 배포가 잘 되었는지 확인하기 위해서 Service 를 NodeType 으로 적용하였다.

nginx-green-svc.yaml

apiVersion: v1
kind: Service
metadata:
  labels:
    app: nginx-green
  name: nginx-green-svc
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
    nodePort: 32080
  selector:
    app: nginx-green
    version: green
  type: NodePort
$ kubectl apply -f nginx-green-svc.yaml

green pod 가 떠 있는 노드를 확인한다.

여기서는 172.31.49.87 노드와 172.31.46.179 노드에 pod 가 생성되어 있다.

$ kubectl get pods -o wide
NAME                           READY   STATUS    RESTARTS   AGE    IP               NODE                                               NOMINATED NODE   READINESS GATES
nginx-blue-69dd468cf4-nts5h    1/1     Running   0          6m7s   10.233.114.152   ip-172-31-49-87.ap-northeast-2.compute.internal    <none>           <none>
nginx-blue-69dd468cf4-xvxqz    1/1     Running   0          6m7s   10.233.110.80    ip-172-31-46-179.ap-northeast-2.compute.internal   <none>           <none>
nginx-green-7df845c6cf-mpj6b   1/1     Running   0          8s     10.233.114.153   ip-172-31-49-87.ap-northeast-2.compute.internal    <none>           <none>
nginx-green-7df845c6cf-pvmrc   1/1     Running   0          8s     10.233.110.81    ip-172-31-46-179.ap-northeast-2.compute.internal   <none>           <none>

curl 명령어로 Green 이 정상적으로 배포되었는지 확인한다.

$ kubectl get svc
NAME                   TYPE           CLUSTER-IP      EXTERNAL-IP                                                                  PORT(S)        AGE
kubernetes             ClusterIP      10.233.0.1      <none>                                                                       443/TCP        12d
nginx-blue-green-svc   LoadBalancer   10.233.30.78    aa1d4e1994e454eb5aea607cfdfd3dcf-23761053.ap-northeast-2.elb.amazonaws.com   80:32234/TCP   5m18s
nginx-green-svc        NodePort       10.233.33.157   <none>                                                                       80:32080/TCP   12s
$ curl 172.31.49.87:32080
<!DOCTYPE html>
<html>
<body style="background-color:green;">
<h1>This is a green webserver</h1>
</body>
</html>

4. Patch 로 Service EndPoint 변경

이제 LoadBalancer 타입으로 생성된 nginx-blue-green-svc 가 Green Pod 로 연결되게 selector 를 Green pod 가 선택되게 변경한다.

$ kubectl patch svc nginx-blue-green-svc -p '{"spec": {"selector": {"app": "nginx-green", "version": "green"}}}'

웹브라우저로 접속하면 Green 으로 변경된 것을 확인할 수 있다.

반응형
Posted by seungkyua@gmail.com
,
반응형

이전에는 aws 에 Kubernetes Cluster 를 설치한 후 Load Balancer 를 연결한는 방법을 설명하였다. Kubernetes Cluster 를 사용하려면 Load Balancer 외에도 필요한 기능이 있는데 그것이 바로 Storage 이다.

Pod 에서 영구적으로 데이터를 저장하기 위해서는 ebs 와 같은 Block Storage 를 생성해서 연결해야 하는데 Kubernetes 에서는 CSI 로 이를 지원하고 있다.

aws 에서 ebs 를 사용하려면 아래의 순서대로 적용한다.

  1. IAM Policy 생성
  2. CSI Driver (Provisioner) 설치
  3. Storage Class 생성
  4. PVC, POD 로 테스트

1. IAM Policy 생성

CSI 를 위한 IAM Policy 는 생성하기 전에 이미 만들어서 제공되고 있는 Managed Policy 를 사용해도 된다. arn:aws:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy

하지만 새로 만든다고 하면 아래와 같이 만들수 있다.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ec2:CreateSnapshot",
        "ec2:AttachVolume",
        "ec2:DetachVolume",
        "ec2:ModifyVolume",
        "ec2:DescribeAvailabilityZones",
        "ec2:DescribeInstances",
        "ec2:DescribeSnapshots",
        "ec2:DescribeTags",
        "ec2:DescribeVolumes",
        "ec2:DescribeVolumesModifications"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "ec2:CreateTags"
      ],
      "Resource": [
        "arn:aws:ec2:*:*:volume/*",
        "arn:aws:ec2:*:*:snapshot/*"
      ],
      "Condition": {
        "StringEquals": {
          "ec2:CreateAction": [
            "CreateVolume",
            "CreateSnapshot"
          ]
        }
      }
    },
    {
      "Effect": "Allow",
      "Action": [
        "ec2:DeleteTags"
      ],
      "Resource": [
        "arn:aws:ec2:*:*:volume/*",
        "arn:aws:ec2:*:*:snapshot/*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "ec2:CreateVolume"
      ],
      "Resource": "*",
      "Condition": {
        "StringLike": {
          "aws:RequestTag/ebs.csi.aws.com/cluster": "true"
        }
      }
    },
    {
      "Effect": "Allow",
      "Action": [
        "ec2:CreateVolume"
      ],
      "Resource": "*",
      "Condition": {
        "StringLike": {
          "aws:RequestTag/CSIVolumeName": "*"
        }
      }
    },
    {
      "Effect": "Allow",
      "Action": [
        "ec2:DeleteVolume"
      ],
      "Resource": "*",
      "Condition": {
        "StringLike": {
          "ec2:ResourceTag/ebs.csi.aws.com/cluster": "true"
        }
      }
    },
    {
      "Effect": "Allow",
      "Action": [
        "ec2:DeleteVolume"
      ],
      "Resource": "*",
      "Condition": {
        "StringLike": {
          "ec2:ResourceTag/CSIVolumeName": "*"
        }
      }
    },
    {
      "Effect": "Allow",
      "Action": [
        "ec2:DeleteVolume"
      ],
      "Resource": "*",
      "Condition": {
        "StringLike": {
          "ec2:ResourceTag/kubernetes.io/created-for/pvc/name": "*"
        }
      }
    },
    {
      "Effect": "Allow",
      "Action": [
        "ec2:DeleteSnapshot"
      ],
      "Resource": "*",
      "Condition": {
        "StringLike": {
          "ec2:ResourceTag/CSIVolumeSnapshotName": "*"
        }
      }
    },
    {
      "Effect": "Allow",
      "Action": [
        "ec2:DeleteSnapshot"
      ],
      "Resource": "*",
      "Condition": {
        "StringLike": {
          "ec2:ResourceTag/ebs.csi.aws.com/cluster": "true"
        }
      }
    }
  ]
}

Role 에 Policy 연결

이전 글에서 설명한 control-plane.cluster-api-provider-aws.sigs.k8s.ionodes.cluster-api-provider-aws.sigs.k8s.io role 에 위의 Policy 를 연결한다.

Role : control-plane.cluster-api-provider-aws.sigs.k8s.io

Role : nodes.cluster-api-provider-aws.sigs.k8s.io

2. CSI Driver (Provisioner) 설치

EBS 용 CSI Driver 를 helm chart repo 를 등록한다.

$ helm repo add aws-ebs-csi-driver https://kubernetes-sigs.github.io/aws-ebs-csi-driver
$ helm repo update

$ helm search repo aws-ebs-csi-driver -l
NAME                                    CHART VERSION   APP VERSION     DESCRIPTION
aws-ebs-csi-driver/aws-ebs-csi-driver   2.16.0          1.15.0          A Helm chart for AWS EBS CSI Driver
aws-ebs-csi-driver/aws-ebs-csi-driver   2.15.1          1.14.1          A Helm chart for AWS EBS CSI Driver
...

지금 최신 버전 차트는 2.16.0 이다. 해당 차트는 Kubernetes 1.17+ 이상만 호환되는데 현재의 웬만한 Kubernetes 버전은 지원된다고 보면 된다.

Helm chart 로 설치한다.

$ helm upgrade -i aws-ebs-csi-driver -n kube-system aws-ebs-csi-driver/aws-ebs-csi-driver --version 2.16.0

설치가 제대로 되었는지는 아래의 명령어로 확인할 수 있다.

3. Storage Class 설치

ebs volume 이 생성될 때 CSI Driver 에게 필요한 설정 값을 전달해야 하는데 이것이 바로 Storage Class 라고 보면 된다.

아래와 같이 생성한다.

$ vi standard-ebs-sc.yaml
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  annotations: 
    storageclass.kubernetes.io/is-default-class: "true"
  name: standard
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Delete

$ kubectl apply -f standard-ebs-sc.yaml

storage class 에 storage type (gp2, gp3, io1 등), iops, 데이터 암호화 여부, 토폴로지 등을 넣을 수 있는데 이를 적용하면 현재 csi driver 에서 에러가 나므로 추가 확인이 필요하다. (볼륨 생성이 안된다던지, 볼륨 attach 가 안된다든지 하는 문제가 발생했는데 자세히 소스까지 찾아보지는 않았음 ㅠㅠ)

storage class 가 잘 생성되었는지 확인한다.

$ kubectl get sc
NAME                 PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
local-path           rancher.io/local-path   Delete          WaitForFirstConsumer   false                  12d
standard (default)   ebs.csi.aws.com         Delete          WaitForFirstConsumer   false                  138m

4. PVC, POD 로 테스트

pvc 를 생성하고 pod 에서 이를 mount 하여 활용해본다.

pvc 생성

$ vi pvc-example.yaml
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: ebs-claim
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: standard
  resources:
    requests:
      storage: 4Gi

$ kubectl apply -f pvc-example.yaml

sc 설정에서 volumeBindingMode: WaitForFirstConsumer 이기 때문에 pod 가 생성되기 전까지는 pv 는 만들어지지 않는다.

POD 생성

$ vi pod-example.yaml
---
apiVersion: v1
kind: Pod
metadata:
  name: app
spec:
  containers:
  - name: app
    image: centos
    command: ["/bin/sh"]
    args: ["-c", "while true; do echo $(date -u) >> /data/out.txt; sleep 5; done"]
    volumeMounts:
    - name: persistent-storage
      mountPath: /data
  volumes:
  - name: persistent-storage
    persistentVolumeClaim:
      claimName: ebs-claim

$ kubectl apply -f pod-example.yaml

out.txt 에 date 값이 설정된 것을 확인할 수 있다.

$ kubectl exec -it app -- cat /data/out.txt
반응형
Posted by seungkyua@gmail.com
,
반응형

kubernetes 에 서비스를 올릴 때 Service 의 Type 으로 LoadBalancer 를 선택하면 cloud 에서 자동으로 LB (external-ip ) 가 생성되어 서비스 pod 에 연결된다. 어떻게 Kubernetes 에서 설정한 값이 cloud 에 연결될까? 이는 Cloud Provider 가 있어 가능하다.

Cloud Provider 는 초기에 Kubernetes Controller 에 포함되어 있다. 하지만 지금은 External Kubernete Cloud Provider 로 Kubernetes 에서 제외되었으며, 이전 Kubernetes Controller 에 포함된 Cloud Provider 는 Legacy Cloud Provider 로 불리고 있다.

AWS Cloud Provider 의 경우에는 아직 1.23 (Kubernetes 와 같이 버전을 맞춰가고 있음) alpha 버전이라 아직은 Legacy Cloud Provider 를 사용하는 것이 안정적이다.

aws 에서는 아래의 순서대로 적용한다.

  1. IAM Policy, Role 생성
  2. VPC, Subnet, Routing Table, Internet Gateway, Nat Gateway 생성
  3. VM 생성
  4. aws resource 에 Tag 적용
  5. Kubernetes Cluster 생성

1. IAM Policy, Role 생성

Control plane 과 Node 2개의 Policy 를 생성한다.

Control Plane Policy

control node 에서 사용할 policy 이다.

정책명: control-plane.cluster-api-provider-aws.sigs.k8s.io

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "autoscaling:DescribeAutoScalingGroups",
        "autoscaling:DescribeLaunchConfigurations",
        "autoscaling:DescribeTags",
        "ec2:DescribeInstances",
        "ec2:DescribeRegions",
        "ec2:DescribeRouteTables",
        "ec2:DescribeSecurityGroups",
        "ec2:DescribeSubnets",
        "ec2:DescribeVolumes",
        "ec2:DescribeAvailabilityZones",
        "ec2:CreateSecurityGroup",
        "ec2:CreateTags",
        "ec2:CreateVolume",
        "ec2:ModifyInstanceAttribute",
        "ec2:ModifyVolume",
        "ec2:AttachVolume",
        "ec2:AuthorizeSecurityGroupIngress",
        "ec2:CreateRoute",
        "ec2:DeleteRoute",
        "ec2:DeleteSecurityGroup",
        "ec2:DeleteVolume",
        "ec2:DetachVolume",
        "ec2:RevokeSecurityGroupIngress",
        "ec2:DescribeVpcs",
        "elasticloadbalancing:AddTags",
        "elasticloadbalancing:AttachLoadBalancerToSubnets",
        "elasticloadbalancing:ApplySecurityGroupsToLoadBalancer",
        "elasticloadbalancing:CreateLoadBalancer",
        "elasticloadbalancing:CreateLoadBalancerPolicy",
        "elasticloadbalancing:CreateLoadBalancerListeners",
        "elasticloadbalancing:ConfigureHealthCheck",
        "elasticloadbalancing:DeleteLoadBalancer",
        "elasticloadbalancing:DeleteLoadBalancerListeners",
        "elasticloadbalancing:DescribeLoadBalancers",
        "elasticloadbalancing:DescribeLoadBalancerAttributes",
        "elasticloadbalancing:DetachLoadBalancerFromSubnets",
        "elasticloadbalancing:DeregisterInstancesFromLoadBalancer",
        "elasticloadbalancing:ModifyLoadBalancerAttributes",
        "elasticloadbalancing:RegisterInstancesWithLoadBalancer",
        "elasticloadbalancing:SetLoadBalancerPoliciesForBackendServer",
        "elasticloadbalancing:AddTags",
        "elasticloadbalancing:CreateListener",
        "elasticloadbalancing:CreateTargetGroup",
        "elasticloadbalancing:DeleteListener",
        "elasticloadbalancing:DeleteTargetGroup",
        "elasticloadbalancing:DescribeListeners",
        "elasticloadbalancing:DescribeLoadBalancerPolicies",
        "elasticloadbalancing:DescribeTargetGroups",
        "elasticloadbalancing:DescribeTargetHealth",
        "elasticloadbalancing:ModifyListener",
        "elasticloadbalancing:ModifyTargetGroup",
        "elasticloadbalancing:RegisterTargets",
        "elasticloadbalancing:DeregisterTargets",
        "elasticloadbalancing:SetLoadBalancerPoliciesOfListener",
        "iam:CreateServiceLinkedRole",
        "kms:DescribeKey"
      ],
      "Resource": [
        "*"
      ]
    }
  ]
}

Node Policy

일반 Node 에 대한 policy 이다.

정책명: nodes.cluster-api-provider-aws.sigs.k8s.io

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ec2:DescribeInstances",
        "ec2:DescribeRegions",
        "ecr:GetAuthorizationToken",
        "ecr:BatchCheckLayerAvailability",
        "ecr:GetDownloadUrlForLayer",
        "ecr:GetRepositoryPolicy",
        "ecr:DescribeRepositories",
        "ecr:ListImages",
        "ecr:BatchGetImage"
      ],
      "Resource": "*"
    }
  ]
}

policy 를 생성했으면 이제 Role 을 생성한다.

Control plane Role

Policy 를 활용할 수 있는 Role 을 만들어서 Policy 와 연결한다.

Role 명: control-plane.cluster-api-provider-aws.sigs.k8s.io

연결할 Policy 리스트
- control-plane.cluster-api-provider-aws.sigs.k8s.io
- nodes.cluster-api-provider-aws.sigs.k8s.io

Node Role

Role 명: nodes.cluster-api-provider-aws.sigs.k8s.io

연결할 Policy 리스트
- nodes.cluster-api-provider-aws.sigs.k8s.io

2. VPC, Subnet, Routing Table, Internet Gateway, Nat Gateway 생성

서울 리전의 경우 VPC 를 1개 만들고, public 용도의 subnet 4개, private 용도의 subnet 4개를 만든다.

Internet Gateway 1개를 만들어서 public subnet 에 연결하고, Nat Gateway 4개를 만들어서 각각 private subnet 에 연결한다.

Routing Table 은 subnet 갯수에 맞는 8개를 만들어서 각각 연결한다. private 용 4개의 Routing table 은 0.0.0.0/0 → nat gateway 를 대상으로 설정하고, public 용 4개의 Routing table 은 0.0.0.0/0 → internet gateway 대상으로 설정한다.

1. vpc : 1개
   - vpc

2. subent : 8개
   - public-subnet-a
   - public-subnet-b
   - public-subnet-c
   - public-subnet-d
   - private-subnet-a
   - private-subnet-b
   - private-subnet-c
   - private-subnet-d

3. internat gateway : 1개
   - igw

4. Nat Gateway : 4개
   - nat-private-a
   - nat-private-b
   - nat-private-c
   - nat-private-d

5. Routing Table : 4개
   - rt-public-a (0.0.0.0/0 -> igw)
   - rt-public-b (0.0.0.0/0 -> igw)
   - rt-public-c (0.0.0.0/0 -> igw)
   - rt-public-d (0.0.0.0/0 -> igw)
   - rt-priabe-a (0.0.0.0/0 -> nat-private-a)
   - rt-priabe-b (0.0.0.0/0 -> nat-private-b)
   - rt-priabe-c (0.0.0.0/0 -> nat-private-c)
   - rt-priabe-d (0.0.0.0/0 -> nat-private-d)

3. VM 생성

VM 은 Controler Plane 3대는 각 private subnet 에 1대씩 생성하고(subnet 1개는 남는다), Node 용 4대는 각 private subnet 1대씩 생성하다.

bastion 노드로 public subnet 에 1대 생성한다.

1. bastion VM 1대
   - public-subnet-a

2. Control Plane VM 3대
   - private-subnet-a
   - private-subnet-b
   - private-subnet-c

3. Node VM 4대
   - private-subnet-a
   - private-subnet-b
   - private-subnet-c
   - private-subnet-d

4. Aws Resource 에 Tag 설정

aws cloud provider 가 리소스를 파악하기 위해서는 aws 에 적절한 값을 설정해야 한다.

4-1. VM 에 IAM Role 을 할당

VM 에서 권한을 얻기 위해서는 반드시 IAM Role 을 할당해야 한다.

Control node 에는 control-plane.cluster-api-provider-aws.sigs.k8s.io role 을 할당한다.

일반 Node 에는 nodes.cluster-api-provider-aws.sigs.k8s.io role 을 할당한다.

4-2 VM 에 Tag 설정

VM 에는 Kubernetes Cluster Name 을 Tag 로 지정한다. kubernetes.io/cluster/<cluster name> 과 같이 지정하는데 Cluster Name 은 Kubernetes 를 설치할 때 지정할 수 있다. 기본 값은 cluster.local 인데 여기서는 ahnsk 로 이름을 지정하였다.

그리고 vm 의 역할을 지정해야 하는데 Controler Node 는 control-plane 으로, Node 는 node 로 지정한다.

4-3. Subnet 에 Tag 설정

Load Balancer 를 핸들링 하기 위해서 Kubernetes Cluster 가 어느 Subnet 과 Routing Table 을 사용해야 하는지 알아야 한다.

주의해야 할 점은 public subnet 의 경우 [kubernetes.io/role/elb](http://kubernetes.io/role/elb) 이지만, private subnet 의 경우에는 [kubernetes.io/role/internal-elb](http://kubernetes.io/role/internal-elb) 로 해야 한다.

4-4. Routing Table 에 Tag 설정

5. Kubernetes Cluster 생성

Kubernetes cluster 이름은 앞에서 설명했듯이 ahnsk 로 설정한다.

kubeadm 혹은 kubespray 를 사용할 수 있으며 여기서 생성 방법을 생략한다.

aws cloud provider 를 활성화 하기 위해서는 API Server, Controller, Kubelet 에 --cloud-provider=aws 옵션을 추가해야 한다.

kube-apiserver.yaml

apiVersion: v1
kind: Pod
metadata:
  annotations:
    kubeadm.kubernetes.io/kube-apiserver.advertise-address.endpoint: 172.31.22.52:6443
  creationTimestamp: null
  labels:
    component: kube-apiserver
    tier: control-plane
  name: kube-apiserver
  namespace: kube-system
spec:
  containers:
  - command:
    - kube-apiserver
    - --advertise-address=172.31.22.52
...
    **- --cloud-provider=aws**
...
    image: registry.k8s.io/kube-apiserver:v1.24.6
...

kube-controller-manager.yaml

apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: null
  labels:
    component: kube-controller-manager
    tier: control-plane
  name: kube-controller-manager
  namespace: kube-system
spec:
  containers:
  - command:
    - kube-controller-manager
    - --allocate-node-cidrs=true
...
    **- --cloud-provider=aws
...**
    image: registry.k8s.io/kube-controller-manager:v1.24.6
...

kubelet.env

...
KUBELET_CLOUDPROVIDER="**--cloud-provider=aws --cloud-config=/etc/kubernetes/cloud_config**"
...

kubelet 은 aws 의 리소스를 위해 추가해야 할 값들이 있는데 cloud_config 을 만들어서 옵션을 전달하였다.

[Global]
zone=
vpc=vpc-b342d5d8
subnetId=
routeTableId=
roleArn=
kubernetesClusterTag=ahnsk
kubernetesClusterId=ahnsk
disableSecurityGroupIngress=false
disableStrictZoneCheck=false
elbSecurityGroup=

값을 다 채워 넣으면 apiserver 가 kube-apiserver-master-dummy 라는 이름으로 잘못 실행되므로 조심해야 한다. master-dummy 로 띄우는 방법은 aws account 가 다를 경우에만 사용하는 방법이다. 이는 아래 소스를 보면 알 수 있다.

https://github.com/kubernetes/legacy-cloud-providers/blob/707ecda639b086132369678680a1b34d4d2b5c7c/aws/aws.go#L1251

...
  tagged := cfg.Global.KubernetesClusterTag != "" || cfg.Global.KubernetesClusterID != ""
    if cfg.Global.VPC != "" && (cfg.Global.SubnetID != "" || cfg.Global.RoleARN != "") && tagged {
        // When the master is running on a different AWS account, cloud provider or on-premise
        // build up a dummy instance and use the VPC from the nodes account
        klog.Info("Master is configured to run on a different AWS account, different cloud provider or on-premises")
        awsCloud.selfAWSInstance = &awsInstance{
            nodeName: "master-dummy",
            vpcID:    cfg.Global.VPC,
            subnetID: cfg.Global.SubnetID,
        }
        awsCloud.vpcID = cfg.Global.VPC
    } else {
        selfAWSInstance, err := awsCloud.buildSelfAWSInstance()
        if err != nil {
            return nil, err
        }
        awsCloud.selfAWSInstance = selfAWSInstance
        awsCloud.vpcID = selfAWSInstance.vpcID
    }
...

RoleARN 의 값을 넣으면 안되는데 값이 없으면서도 어떻게 kubelet 이 해당 Role 로 인증을 받을 수 있을까? 이는 앞에서 설명한 VM 에 IAM Role 인 control-plane.cluster-api-provider-aws.sigs.k8s.io 이나 nodes.cluster-api-provider-aws.sigs.k8s.io 이 설정되어 있기 때문에 가능하다.

kubelet-config.yaml

kubernetes node 정보에 providerID 값이 들어가 있어야 한다. 만약 이 정보가 없다면 LoadBalancer 가 생성된다고 하더라도 인스턴스가 LoadBalancer 에 할당되지 않아 제대로 사용할 수 가 없다.

providerID 는 kubelet-config.yaml 에 추가한다. aws:///<zone-id>/<instance-id> 값으로 추가한다.

$ sudo vi /etc/kubernetes/kubelet-config.yaml
...
providerID: "aws:///ap-northeast-2d/i-0f59f059e2d64213f"
...

이미 노드가 생성된 경우에는 해당 값을 변경하고 kubelet 서비스를 다시 restart 한다고 값이 추가되지는 않는다. 그래서 patch 명령으로 동적으로 추가하는 것이 좋다.

$ kubectl patch node ip-172-31-46-179.ap-northeast-2.compute.internal -p '{"spec": {"providerID": "aws:///ap-northeast-2d/i-08cb6f884239f894c"}}'

5. Nginx 로 테스트

nginx 를 생성하고 service 를 LoadBalancer type 으로 생성하여 잘 접속이 되는지 확인해 보자.

$ kubectl create deploy nginx --image=nginx
deployment.apps/nginx created
$ kubectl get pods
NAME                    READY   STATUS    RESTARTS   AGE
nginx-8f458dc5b-nhkfd   1/1     Running   0          55s
$ kubectl expose deployment nginx --name nginx-svc --target-port=80 --port=80 --type=LoadBalancer
$ kubectl get svc
NAME         TYPE           CLUSTER-IP     EXTERNAL-IP                                                                   PORT(S)        AGE
kubernetes   ClusterIP      10.233.0.1     <none>                                                                        443/TCP        5h28m
nginx-svc    LoadBalancer   10.233.7.131   a76a315f5d7c14652a4db83cc3b25125-113127685.ap-northeast-2.elb.amazonaws.com   80:30796/TCP   27s
반응형
Posted by seungkyua@gmail.com
,
반응형

MLOps 라고 불리는 s/w 가 여러개 있는데 그 중에서 Kubeflow 는 Kubernetes 기반의 MLOps 를 쉽게 구축할 수 있는 오픈 소스이다. 초창기 개발은 Google 이 주축이 되어 Arrikto 가 같이 참여하여 개발하는 형식이었는데 이제는 많은 글로벌 회사에서 같이 참여하여 점차 확대되고 있는 추세이다.

Kubeflow 는 Kubernetes 위에서만 돌아가기 때문에 Kubernetes 를 알아야 한다는 단점이 있지만, 일단 Kubernets 를 알고 있다면 설치가 아주 쉽다. 물론 그 안에 들어가는 컴포넌트들이 많고, MLOps 의 특성상 자동화는 workflow 를 잘 작성해서 pipeline 을 어떻게 구성하느냐가 중요하기 때문에 어려운 사용법을 익혀야 한다.

KServe 는 Kubeflow 의 여러 기능 중에서 ML Model Serving 에 해당하는 컴포넌트이며, 얼마전 kubeflow 내의 KFServe 컴포넌트 이었다가 독립적인 Add-Ons 으로 빠져 나오면서 KServe 로 이름을 바꾸고 자체 github repository 를 만들었다.
 
 

KServe Architecture

KServe 아키텍처는 다음과 같다.

[출처: https://www.kubeflow.org/docs/external-add-ons/kserve/kserve/]
 
 
그림과 같이 런타임으로 TensorFlow, PYTORCH, SKLearn, XGBoost, ONNX 등 다양한 모델 프레임워크를 지원하며 필요하면 커스텀 런타임을 만들어서 지원할 수 도 있다.

KServe 하단에는 Knative 와 Istio (Serverless Layer) 를 갖을 수 있는데 하단에는 다음과 같이 구성할 수 있다.

  1. KServe + Knative + Istio
  2. KServe + Istio

Knative 는 옵션이기는 하나 Knative 를 설치하면서 로깅 (fluentbit + ElasticSearch + Kibana), 모니터링 (Prometheus, Exporter), 트레이싱(Jaeger + ElasticSearch) 을 쉽게 연결할 수 있다는 장점이 있다. 또한 Istio 가 제공하는 Network 핸들링 기능을 쉽게 사용할 수 있다.

KServe 설치는 Istio 설치 → Knative 설치 → KServe 설치 순으로 진행하며, 이에 맞는 버전은 다음과 같다.
 
 

Recommended Version Matrix

Kubernetes Version Istio Version Knative Version
1.20 1.9, 1.10, 1.11 0.25, 0.26, 1.0
1.21 1.10, 1.11 0.25, 0.26, 1.0
1.22 1.11, 1.12 0.25, 0.26, 1.0

여기서는 Kubernetes 1.22 에 맞춰서 설치한다.
 
 

Istio 설치

Istio 는 Service Mesh 를 쉽게 구성가능하도록 지원하는 플랫폼으로 proxy 가 sidecar 형태로 추가되어 네트워크를 조절할 수 있다. 네트워크를 조절기능의 대표적인 것은 네트워크 쉬프팅이 있다. Canary Release 나 A/B Test 에서는 서로 다른 버전의 서비스로 호출되는 네트워크의 흐름 비중을 조절가능해야 한다.

이런 이유로 요즘 Service Mesh 는 Sidecar 활용 패턴을 사용하는데 Istio 에서는 서비스 배포 시에 Sidecar 를 자동으로 Injection 해주는 기능을 지원하고 있으며, 많은 곳에서 대부분 auto injeciton 을 사용하고 있다.

하지만 Knative 에서는 auto injection 을 사용하지 않는다. auto injection 은 kubernetes namespace 에 label 을 추가하여 (istio-injeciton=enabled) 자동으로 해당 namespace 에 배포되는 pod 에는 sidecar proxy 가 자동으로 설치되는 기능이라, Service Mesh 를 사용하고 싶지 않은 서비스들에게도 영향을 줄 수 있기 때문에 auto injection 을 disable 할 것을 권장하고 있다.

Istio 설치는 helm chart 로 쉽게 설치할 수 있다.

helm repo 를 추가하고 value 값을 오버라이드할 파일을 만든다.

$ helm repo add istio https://istio-release.storage.googleapis.com/charts
$ helm repo update

$ vi istiod_1.12.8_default_values.yaml
global:
  proxy:
        autoInject: disabled   # 원래 값은 enabled 임

 
 
Istio-system 네임스페이스를 생성한 후 helm chart 를 설치한다.

base 는 crd 를 설치하며, istiod 가 실제 데몬 서비스다.

$ kubectl create namespace istio-system
$ helm upgrade -i istio-base istio/base --version 1.12.8 -n istio-system -f istiod_1.12.8_default_values.yaml
$ helm upgrade -i istiod istio/istiod --version 1.12.8 -n istio-system -f istiod_1.12.8_default_values.yaml --wait

 
 
외부에서 서비스로 접근하기 위한 North - South 통신은 Istio Ingress Gateway 를 통해서 가능하다. 그러므로 Istio Gateway 를 추가로 설치해 준다.

먼저, value 값을 오버라이드할 파일을 만든다.

$ vi gateway_1.12.8_default_values.yaml
podAnnotations:
  prometheus.io/port: "15020"
  prometheus.io/scrape: "true"
  prometheus.io/path: "/stats/prometheus"
  inject.istio.io/templates: "gateway"
  sidecar.istio.io/inject: "true" 

 
 
istio-ingress 네임스페이스를 생성하고 istio ingress gateway 를 helm chart로 설치한다.

$ kubectl create ns istio-ingress
$ helm upgrade -i istio-ingress istio/gateway --version 1.12.8 -n istio-ingress -f gateway_1.12.8_default_values.yaml

 
 
아래와 같이 잘 설치되어 있음을 확인할 수 있다.

$ kubectl get pods -n istio-system
NAME                      READY   STATUS    RESTARTS   AGE
istiod-68d7bfb6d8-nt82m   1/1     Running   0          20d

$ kubectl get pods -n istio-ingress
NAME                             READY   STATUS    RESTARTS   AGE
istio-ingress-69495c6667-7njv8   1/1     Running   0          20d

 
 

Knative 설치

Knative 는 Serverless 플랫폼이라 생각하면 된다. 서비스를 Istio 를 활용하여 배포하면 Gateway, VirtualServie 를 만들어서 연결해야 하는데 Knative 를 이를 자동으로 생성해주기 때문에 편리하다. 또한 앞에서도 설명한 모니터링, 로깅, 트레이싱이 잘 연결되기 때문에 일단 설치를 한다면 사용하기 편리하다.

Knative 는 설치 모듈이 Serving 과 Eventing 2개로 나눠져 있다. 일단 API 서비스가 가능한 Serving 모듈만 설치하고 테스트를 한다. 또한 모니터링, 로깅, 트레이싱도 다음에 설명하고 지금은 Knative Serving 기능에 집중한다.

Knative 는 Yaml 과 Operator 로 설치할 수 있는데 공식 문서에서 Operator 는 개발/테스트 환경에서만 사용하라고 권고하기 때문에 yaml 로 설치한다.

먼저, crd 를 설치하고, Serving 모듈을 설치한다.

$ kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.5.0/serving-crds.yaml
--- output ---
customresourcedefinition.apiextensions.k8s.io/certificates.networking.internal.knative.dev created
customresourcedefinition.apiextensions.k8s.io/configurations.serving.knative.dev created
customresourcedefinition.apiextensions.k8s.io/clusterdomainclaims.networking.internal.knative.dev created
customresourcedefinition.apiextensions.k8s.io/domainmappings.serving.knative.dev created
customresourcedefinition.apiextensions.k8s.io/ingresses.networking.internal.knative.dev created
customresourcedefinition.apiextensions.k8s.io/metrics.autoscaling.internal.knative.dev created
customresourcedefinition.apiextensions.k8s.io/podautoscalers.autoscaling.internal.knative.dev created
customresourcedefinition.apiextensions.k8s.io/revisions.serving.knative.dev created
customresourcedefinition.apiextensions.k8s.io/routes.serving.knative.dev created
customresourcedefinition.apiextensions.k8s.io/serverlessservices.networking.internal.knative.dev created
customresourcedefinition.apiextensions.k8s.io/services.serving.knative.dev created
customresourcedefinition.apiextensions.k8s.io/images.caching.internal.knative.dev created

$ kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.5.0/serving-core.yaml
--- output ---
namespace/knative-serving created
clusterrole.rbac.authorization.k8s.io/knative-serving-aggregated-addressable-resolver created
clusterrole.rbac.authorization.k8s.io/knative-serving-addressable-resolver created
clusterrole.rbac.authorization.k8s.io/knative-serving-namespaced-admin created
clusterrole.rbac.authorization.k8s.io/knative-serving-namespaced-edit created
clusterrole.rbac.authorization.k8s.io/knative-serving-namespaced-view created
clusterrole.rbac.authorization.k8s.io/knative-serving-core created
clusterrole.rbac.authorization.k8s.io/knative-serving-podspecable-binding created
serviceaccount/controller created
clusterrole.rbac.authorization.k8s.io/knative-serving-admin created
clusterrolebinding.rbac.authorization.k8s.io/knative-serving-controller-admin created
clusterrolebinding.rbac.authorization.k8s.io/knative-serving-controller-addressable-resolver created
customresourcedefinition.apiextensions.k8s.io/images.caching.internal.knative.dev unchanged
customresourcedefinition.apiextensions.k8s.io/certificates.networking.internal.knative.dev unchanged
customresourcedefinition.apiextensions.k8s.io/configurations.serving.knative.dev unchanged
customresourcedefinition.apiextensions.k8s.io/clusterdomainclaims.networking.internal.knative.dev unchanged
customresourcedefinition.apiextensions.k8s.io/domainmappings.serving.knative.dev unchanged
customresourcedefinition.apiextensions.k8s.io/ingresses.networking.internal.knative.dev unchanged
customresourcedefinition.apiextensions.k8s.io/metrics.autoscaling.internal.knative.dev unchanged
customresourcedefinition.apiextensions.k8s.io/podautoscalers.autoscaling.internal.knative.dev unchanged
customresourcedefinition.apiextensions.k8s.io/revisions.serving.knative.dev unchanged
customresourcedefinition.apiextensions.k8s.io/routes.serving.knative.dev unchanged
customresourcedefinition.apiextensions.k8s.io/serverlessservices.networking.internal.knative.dev unchanged
customresourcedefinition.apiextensions.k8s.io/services.serving.knative.dev unchanged
image.caching.internal.knative.dev/queue-proxy created
configmap/config-autoscaler created
configmap/config-defaults created
configmap/config-deployment created
configmap/config-domain created
configmap/config-features created
configmap/config-gc created
configmap/config-leader-election created
configmap/config-logging created
configmap/config-network created
configmap/config-observability created
configmap/config-tracing created
horizontalpodautoscaler.autoscaling/activator created
poddisruptionbudget.policy/activator-pdb created
deployment.apps/activator created
service/activator-service created
deployment.apps/autoscaler created
service/autoscaler created
deployment.apps/controller created
service/controller created
deployment.apps/domain-mapping created
deployment.apps/domainmapping-webhook created
service/domainmapping-webhook created
horizontalpodautoscaler.autoscaling/webhook created
poddisruptionbudget.policy/webhook-pdb created
deployment.apps/webhook created
service/webhook created
validatingwebhookconfiguration.admissionregistration.k8s.io/config.webhook.serving.knative.dev created
mutatingwebhookconfiguration.admissionregistration.k8s.io/webhook.serving.knative.dev created
mutatingwebhookconfiguration.admissionregistration.k8s.io/webhook.domainmapping.serving.knative.dev created
secret/domainmapping-webhook-certs created
validatingwebhookconfiguration.admissionregistration.k8s.io/validation.webhook.domainmapping.serving.knative.dev created
validatingwebhookconfiguration.admissionregistration.k8s.io/validation.webhook.serving.knative.dev created
secret/webhook-certs created

 
 
다음은 Isito 와 연동하기 위한 network 들을 설치한다.

$ kubectl apply -f https://github.com/knative/net-istio/releases/download/knative-v1.5.0/net-istio.yaml

 
 
그대로 설치하면 Isito gateway 와 연동되지 않는다. 그렇기 때문에 아래와 같이 selector 를 수정해 줘야 한다.

$ kubectl edit gateway -n knative-serving knative-ingress-gateway
...
spec:
  selector:
    istio: ingressgateway
    istio: ingress          # 추가

$ kubectl edit gateway -n knative-serving knative-local-gateway
...
spec:
  selector:
    istio: ingressgateway
    istio: ingress          # 추가

 
 

Istio 의 Ingress gateway 앞단에는 LoadBalancer 가 연결되어 있다. LoadBalancer 가 External IP 로 연결되어 있으면 IP 를 dns 로 연결해 주는 magic dns (sslip.io) 를 사용할 수 있고, LoadBalancer 가 domain name 으로 연결되어 있으면 실제 DNS 에 CNAME 을 등록하여 연결하면 된다.

$ kubectl get svc -n istio-ingress
NAME            TYPE           CLUSTER-IP       EXTERNAL-IP 
istio-ingress   LoadBalancer   10.107.111.229   xxxxx.ap-northeast-2.elb.amazonaws.com

 
 

여기서는 aws 를 사용하고 있기 때문에 Route53 에 CNAME 을 등록하였다.

서비스 도메인: helloworld-go-default.taco-cat.xyz
target: xxxxx.ap-northeast-2.elb.amazonaws.com
type: CNAME

 
 

Knative ConfigMap 설정

마지막으로 Knative ConfigMap 에 기본 도메인과 full 도메인 설정을 세팅한다.

이 설정은 앞서 Route53 에 등록한 서비스 도메인과 같은 형식으로 설정되게 구성해야 한다.

## Domain: taco-cat.xyz
$ kubectl edit cm config-domain -n knative-serving
apiVersion: v1
data:
  taco-cat.xyz: ""
kind: ConfigMap
[...]

## Name: helloworld-go
## Namesapce: default
## Domain: taco-cat.xyz
$ kubectl edit cm config-network -n knative-serving
apiVersion: v1
data:
  domain-template: "{{.Name}}-{{.Namespace}}.{{.Domain}}"

 
 

Knative sample 배포 테스트

Knative 에서 제공하는 helloworld-go 샘플 프로그램을 배포해 보자.

서비스 이름이 helloworld-go, Namespace 가 default 로 앞서 Route53 및 ConfigMap 에 설정한 도메인 형식과 동일함을 할 수 있다.

$ git clone https://github.com/knative/docs knative-docs
$ cd knative-docs/code-samples/serving/hello-world/helloworld-go

$ vi service.yaml
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: helloworld-go
  namespace: default
spec:
  template:
    spec:
      containers:
      - image: gcr.io/knative-samples/helloworld-go
        env:
        - name: TARGET
          value: "Go Sample v1"

$ kubectl apply -f service.yaml

 
 

배포가 제대로 되었는지 확인해 보자.

$ kubectl get route
NAME            URL                                         READY   REASON
helloworld-go   http://helloworld-go-default.taco-cat.xyz   True

 
 

Knative 는 zero replicas 를 사용한다

Serverless 를 어떻게 구현했을까? 사실 Knative 는 Kubernetes 의 zero replicas 를 사용했다.

배포 후에 deployment 를 조회하면 아래과 같이 Ready 와 Available 이 0 상태임을 알 수 있다.

$ kubectl get deploy
NAME                                       READY   UP-TO-DATE   AVAILABLE   AGE
helloworld-go-00001-deployment             0/0     0            0           10d

 
 

배포도 잘되고 Route53 에 dns 도 어느 정도 시간이 지났다면 브라우저 혹은 curl 로 확인할 수 있다.

$ curl http://helloworld-go-default.taco-cat.xyz
--- output ---
Hello Go Sample v1!

 
 

이렇게 요청이 들어오면 실제로 pod 가 실행되고 있음을 알 수 있다. 1분 동안 아무런 요청이 없으면 pod 는 다시 사라지고 대기 상태가 된다. (요청이 없더라도 중간에 다시 pod 가 생겨서 실제로는 일정 시간 동안 새로운 pod 로 교체된다)

$ kubectl get deploy
NAME                                       READY   UP-TO-DATE   AVAILABLE   AGE
helloworld-go-00001-deployment             1/1     1            1           10d

 
 

TLS 인증서 적용

인증서를 가지고 있다면 gateway 에 tls 를 적용하여 tls termination 을 할 수 있다.

아래는 istio-ingress 네임스페이스에 secret 으로 taco-cat-tls 인증서를 설치한 후 gateway 에서 해당 인증서를 읽을 수 있도록 tls 를 추가한 부분이다.

$ kubectl edit gateway knative-ingress-gateway -n knative-serving
...
spec:
  selector:
    istio: ingress
  servers:
  - hosts:
    - '*'
    port:
      name: http
      number: 80
      protocol: HTTP
  - hosts:
    - '*.taco-cat.xyz'
    port:
      name: https
      number: 443
      protocol: HTTPS
    ## tls 추가
    tls:
      mode: SIMPLE
      credentialName: taco-cat-tls

 
 

HPA 설치

서비스에 요청이 신규로 들어오거나, 많아지면 replicas 수를 조절하여 pod 를 실행해주는 activator 가 있다. 이 activator 를 auto scaling 하는 hpa 를 설치한다.

$ kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.5.0/serving-hpa.yaml

 
 

hpa 를 조회해서 확인할 수 있다.

$ kubectl get hpa -n knative-serving
NAME        REFERENCE              TARGETS   MINPODS   MAXPODS   REPLICAS   AGE
activator   Deployment/activator   0%/100%   1         20        1          13d
webhook     Deployment/webhook     3%/100%   1         5         1          13d

 
 

KServe 설치

KServe 는 이제 막 Helm chart 가 만들어지고 있다. 그렇기 때문에 일단은 yaml 로 설치를 진행한다.

먼저 KServe 컴포넌트를 설치하고 이어서 각종 ML Framework 를 나타내는 Runtime을 설치한다.

$ kubectl apply -f https://github.com/kserve/kserve/releases/download/v0.8.0/kserve.yaml
--- output ---
namespace/kserve created
customresourcedefinition.apiextensions.k8s.io/clusterservingruntimes.serving.kserve.io created
customresourcedefinition.apiextensions.k8s.io/inferenceservices.serving.kserve.io created
customresourcedefinition.apiextensions.k8s.io/servingruntimes.serving.kserve.io created
customresourcedefinition.apiextensions.k8s.io/trainedmodels.serving.kserve.io created
serviceaccount/kserve-controller-manager created
role.rbac.authorization.k8s.io/leader-election-role created
clusterrole.rbac.authorization.k8s.io/kserve-manager-role created
clusterrole.rbac.authorization.k8s.io/kserve-proxy-role created
rolebinding.rbac.authorization.k8s.io/leader-election-rolebinding created
clusterrolebinding.rbac.authorization.k8s.io/kserve-manager-rolebinding created
clusterrolebinding.rbac.authorization.k8s.io/kserve-proxy-rolebinding created
configmap/inferenceservice-config created
configmap/kserve-config created
secret/kserve-webhook-server-secret created
service/kserve-controller-manager-metrics-service created
service/kserve-controller-manager-service created
service/kserve-webhook-server-service created
statefulset.apps/kserve-controller-manager created
certificate.cert-manager.io/serving-cert created
issuer.cert-manager.io/selfsigned-issuer created
mutatingwebhookconfiguration.admissionregistration.k8s.io/inferenceservice.serving.kserve.io created
validatingwebhookconfiguration.admissionregistration.k8s.io/inferenceservice.serving.kserve.io created
validatingwebhookconfiguration.admissionregistration.k8s.io/trainedmodel.serving.kserve.io created

$ kubectl apply -f https://github.com/kserve/kserve/releases/download/v0.8.0/kserve-runtimes.yaml
--- output---
clusterservingruntime.serving.kserve.io/kserve-lgbserver created
clusterservingruntime.serving.kserve.io/kserve-mlserver created
clusterservingruntime.serving.kserve.io/kserve-paddleserver created
clusterservingruntime.serving.kserve.io/kserve-pmmlserver created
clusterservingruntime.serving.kserve.io/kserve-sklearnserver created
clusterservingruntime.serving.kserve.io/kserve-tensorflow-serving created
clusterservingruntime.serving.kserve.io/kserve-torchserve created
clusterservingruntime.serving.kserve.io/kserve-tritonserver created
clusterservingruntime.serving.kserve.io/kserve-xgbserver created

 
 

KServe 설치를 확인한다.

$ kubectl get pod -n kserve
NAME                          READY   STATUS    RESTARTS   AGE
kserve-controller-manager-0   2/2     Running   0          7d10h

 
 

Rumtime 도 설치되었는지 확인한다.

$ kubectl get clusterservingruntimes
NAME                        DISABLED   MODELTYPE    CONTAINERS         AGE
kserve-lgbserver                       lightgbm     kserve-container   7d10h
kserve-mlserver                        sklearn      kserve-container   7d10h
kserve-paddleserver                    paddle       kserve-container   7d10h
kserve-pmmlserver                      pmml         kserve-container   7d10h
kserve-sklearnserver                   sklearn      kserve-container   7d10h
kserve-tensorflow-serving              tensorflow   kserve-container   7d10h
kserve-torchserve                      pytorch      kserve-container   7d10h
kserve-tritonserver                    tensorrt     kserve-container   7d10h
kserve-xgbserver                       xgboost      kserve-container   7d10h

 
 

Sample model 을 KServe 를 활용하여 배포

tensorflow 로 개발된 mnist 샘플 모델을 KServe 로 배포해 보자.

KServe 는 model in load 패턴을 적용하여 인퍼런스 서비스를 수행한다. 아래에서는 모델이 gs 에 저장되어 있으면 이를 가져와서 서빙하는 구조이다.

runtime 은 앞서 설치한 clusterservingruntime 중에 하나인 kserve-tensorflow-serving 이고 버전이 2 임을 알 수 있다.

$ vi mnist_tensorflow.yaml
---
apiVersion: "serving.kserve.io/v1beta1"
kind: "InferenceService"
metadata:
  name: "mnist"
spec:
  predictor:
    model:
      modelFormat:
        name: tensorflow
        version: "2"
      storageUri: "gs://kserve/models/mnist"
      runtime: kserve-tensorflow-serving
    logger:
      mode: all

$ kubectl apply -f mnist_tensorflow.yaml

 
 

서빙 배포 확인은 다음과 같다.

KServe 에서 도메인을 만들 때 namespace 를 추가로 붙히기 때문에 도메인이 아래와

$ kubectl get isvc
NAME    URL     READY   PREV   LATEST   PREVROLLEDOUTREVISION  LATESTREADYREVISION
mnist   http://mnist-default.taco-cat.xyz   True           100 mnist-predictor-default-00001

$ kubectl get route
NAME                      URL                                                   READY   REASON
mnist-predictor-default   http://mnist-predictor-default-default.taco-cat.xyz   True

 
 

Route53 에 도메인을 추가한다.

서비스 도메인: mnist-predictor-default-default.taco-cat.xyz
target: xxxxx.ap-northeast-2.elb.amazonaws.com
type: CNAME

 
 

아래와 같이 요청하여 결과값이 제대로 나오는지 확인한다.

$ curl https://mnist-predictor-default-default.taco-cat.xyz/v1/models/mnist:predict \
   -H 'Content-Type: application/json' \
   -d @mnist.json
--- output ---
{
    "predictions": [[3.2338352e-09, 1.66207215e-09, 1.17224181e-06, 0.000114716699, 4.34008705e-13, 4.64885304e-08, 3.96761454e-13, 0.999883413, 1.21785089e-08, 6.44099089e-07]
    ]
}

 
 

반응형
Posted by seungkyua@gmail.com
,
반응형

Pod 가 새롭게 생성되면 Scheduler 를 통해서 Pod 가 실행될 노드가 결정된다. 즉 모든 Pod 는 Scheduler 에 의해서 최적의 노드가 결정되고 실행된다.

 

kube-scheduler 는 Control Plane 에 속하는 컴포넌트 중에 하나이며, 디폴트 scheduler 이다. 만약 특별한 스케줄링을 원한다며 커스텀 스케줄러를 만들어서 대체할 수 도 있다.

 

Pod 가 스케줄링 되기 위해서는 노드가 스케줄링 요구 사항에 맞아야 하는데 이 때, 요구사항에 맞는 노드를 피저블 노드(feasible node) 라고 한다.

 

스케줄러는 피저블 노드를 찾고 적절한 로직을 통해 노드에 점수를 부여한다. 가장 최고 점수를 갖는 노드에 Pod 가 실행된다.

스케줄링을 결정하는 데에는 하드웨어, 소프트웨어, policy contraints, affinity 와 anti-affinity spec, local data 등 여러 요구 사항들이 있다.

 

kube-scheduler 가 결정하는 2-step operation

  1. Filtering
  2. Scoring

Filtering 단계에서는 피저블 노드를 찾는 단계이다. 예를 들면, PodFitsResources 필터는 Pod 의 request 리소스를 만족하는 리소스를 갖는 후보 노드를 추려낸다. 이 단계 이후에는 적합한 노드 리스트가 도출되는데 만약 노드 리스트의 값이 비었다면 Pod 는 스케줄링 되지 않는다.

Scoring 단계에서는 필터링된 노드 리스트 중에서 Pod 가 실행될 최적의 노드를 찾는다. 스케줄러는 노드 리스트에 있는 각 노드들에 점수를 부여한다.

마지막으로 kube-scheduler 는 최고 점수의 노드에 Pod 를 배치한다.

Filtering 과 Scoring 을 설정할 수 있는 방법은 2가지가 있다.

  1. Scheduling Policies (kubernetes 1.22 버전까지만 사용)
    • Predicates : filtering 을 조절할 수 있음
    • Priorities: scoring 을 조절할 수 있음
  2. Scheduling Profiles (https://kubernetes.io/docs/reference/scheduling/config/#profiles)
    • KubeSchedulerConfiguration 리소스 타입으로 조절할 수 있음
    • plugins 를 통해 Filter 와 Score 를 조절 가능

 

nodeSelector

Node 에 label 을 설정하고 Pod 에서 nodeSelector 로 Node 지정하는 방식이다.

$ kubectl get nodes -L nginx
NAME          STATUS   ROLES                  AGE   VERSION   NGINX
k1-master01   Ready    control-plane,master   92d   v1.21.6
k1-node01     Ready    ingress                92d   v1.21.6
k1-node02     Ready    <none>                 92d   v1.21.6   enabled
k1-node03     Ready    <none>                 92d   v1.21.6   enabled
k1-node04     Ready    <none>                 92d   v1.21.6
k1-node05     Ready    <none>                 92d   v1.21.6

k1-node02 와 k1-node03 에 nginx=enabled 로 label 이 지정되어 있다.

 

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-nodeselector
  namespace: kube-sample
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.21.0
        ports:
        - containerPort: 80
      nodeSelector:
        nginx: enabled

nodeSelector 를 활용하여 deployment 를 배포하면 아래와 같이 k1-node02 혹은 k1-node03 에 배포된다.

 

$ kubectl get pods -n kube-sample -o wide
NAME                                  READY   STATUS    RESTARTS   AGE   IP             NODE        NOMINATED NODE   READINESS GATES
nginx-nodeselector-68977d7759-sf48l   1/1     Running   0          9s    10.233.113.7   k1-node02   <none>           <none>

 

Node affinity

nodeSelector 와 동작 방식이 유사하지만 다음과 같은 차이가 있다.

requiredDuringSchedulingIgnoredDuringExecution: 스케줄링에서 만족하는 노드가 없으면 스케줄링 되지 않는다.

preferredDuringSchedulingIgnoredDuringExecution: 스케줄링에서 만족하는 노드를 찾고 노드가 없더라도 스케줄링은 된다. weight 필드의 값을 scoring 에 포함시킨다.

 

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-node-affinity
  namespace: kube-sample
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: nginx
                operator: In
                values:
                - enabled
            - matchExpressions:
              - key: kubernetes.io/os
                operator: In
                values:
                - windows
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 1
            preference:
              matchExpressions:
              - key: another-node-label-key
                operator: In
                values:
                - another-node-label-value
      containers:
      - name: nginx
        image: nginx:1.21.0
        ports:
        - containerPort: 80

위와 같이 nodeSelectorTerms 에 여러 개의 matchExpressions 가 있으면 이 중 하나만 만족해도 스케줄링은 된다. nodeSelectorTerms 에 하나의 matchExpressions 가 있고, key value 가 여러 개가 있으면 matchExpressions 내의 모든 조건을 만족해야 스케줄링이 된다.

operator 로 쓸 수 있는 값은 In, NotIn, Exists, DoesNotExist, Gt, Lt 이다.

NotInDoesNotExist 는 node anti-affinity 로 사용 가능하다.

 

$ kubectl get pods -n kube-sample -o wide
NAME                                   READY   STATUS        RESTARTS   AGE    IP              NODE        NOMINATED NODE   READINESS GATES
nginx-node-affinity-755cf7f85d-ss9q5   1/1     Running       0          5s     10.233.113.10   k1-node02   <none>           <none>

 

Pod affinity and anti-affinity

Pod 를 서로 다른 노드에 배치시키는 방법은 pod anti-affinity 를 통해서 구현할 수 있다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-pod-anti-affinity
  namespace: kube-sample
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx-pod-anti-affinity
  template:
    metadata:
      labels:
        app: nginx-pod-anti-affinity
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - nginx-pod-anti-affinity
            topologyKey: kubernetes.io/hostname
      containers:
      - name: nginx
        image: nginx:1.21.0
        ports:
        - containerPort: 80

위와 같이 pod label 에 pod anti affinity 를 설정하면 replicas 값 2 에 의해서 생성되는 2개의 pod 는 서로 다른 노드에 배치된다. topologyKey 는 kubernetes.io/hostname 값으로 세팅하고 특정 node lable 로 하고 싶으면 node affinity 를 추가하면 된다.

 

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-pod-anti-affinity
  namespace: kube-sample
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx-pod-anti-affinity
  template:
    metadata:
      labels:
        app: nginx-pod-anti-affinity
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - nginx-pod-anti-affinity
            topologyKey: kubernetes.io/hostname
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: nginx
                operator: In
                values:
                - enabled
      containers:
      - name: nginx
        image: nginx:1.21.0
        ports:
        - containerPort: 80
$ kubectl get pods -n kube-sample -o wide
NAME                                       READY   STATUS    RESTARTS   AGE   IP              NODE        NOMINATED NODE   READINESS GATES
nginx-pod-anti-affinity-7cfcbcff8f-c2429   1/1     Running   0          14s   10.233.96.3     k1-node03   <none>           <none>
nginx-pod-anti-affinity-7cfcbcff8f-dmqqm   1/1     Running   0          14s   10.233.113.12   k1-node02   <none>           <none>

 

Taint 와 Toleration

노드에 taint 를 지정하면 해당 노드에 taint effect 를 줄 수 있다. 예를 들어 taint effect 에 NoSchedule 을 준다면 해당 노드에는 pod 가 스케줄링 되지 않는다.

taint effect 를 제거할 수 있는 방법은 Pod 에 toleration 을 세팅하는 것이다.

마스터에는 기본적으로 taint 를 지정해서 스케줄링 안되게 세팅해 놓는 것을 알 수 있다.

 

$ kubectl describe node k1-master01

Name:               k1-master01
Roles:              control-plane,master
Labels:             beta.kubernetes.io/arch=amd64
                    beta.kubernetes.io/os=linux
                    kubernetes.io/arch=amd64
                    kubernetes.io/hostname=k1-master01
                    kubernetes.io/os=linux
                    node-role.kubernetes.io/control-plane=
                    node-role.kubernetes.io/master=
                    node.kubernetes.io/exclude-from-external-load-balancers=
Annotations:        kubeadm.alpha.kubernetes.io/cri-socket: /var/run/containerd/containerd.sock
                    node.alpha.kubernetes.io/ttl: 0
                    projectcalico.org/IPv4Address: 192.168.30.13/24
                    volumes.kubernetes.io/controller-managed-attach-detach: true
CreationTimestamp:  Tue, 08 Feb 2022 21:27:12 +0900
Taints:             node-role.kubernetes.io/master:NoSchedule

마지막의 Taints 값은 kubectl 로 세팅할 수 있다.

 

$ kubectl taint nodes k1-master01 node-role.kubernetes.io/master=:NoSchedule

그럼 master 노드에 toleration 을 활용하여 pod 를 배치해 보자.

 

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-toleration
  namespace: kube-sample
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx-toleration
  template:
    metadata:
      labels:
        app: nginx-toleration
    spec:
      tolerations:
      - key: node-role.kubernetes.io/master
        operator: Exists
        effect: NoSchedule
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: node-role.kubernetes.io/control-plane
                operator: Exists
      containers:
      - name: nginx
        image: nginx:1.21.0
        ports:
        - containerPort: 80

toleration 으로 taint 효과를 없애고 node affinity 를 활용하여 control-plane, 즉 master 노드에 pod 를 배치 시켰다. 물론 master 노드에 node-role.kubernetes.io/control-plane 이라는 label 이 존재해야 한다.

 

taint effect 는 NoSchedule, NoExecute, PreferNoSchedule 이 있다.

 

NoSchedule 은 스케줄링을 막는 것이고, NoExecute 는 실행중인 pod 를 eviction 시킨다. PreferNoSchedule 은 가능하면 스케줄링 하지 않는다는 의미이다.

 

operator 에는 Exists 와 Equal 이 있다. Exists 는 key 값이 존재하는 지를 판단하기 때문에 value 값이 필요 없으며, Equal 은 key 와 value 값이 지정된 값과 일치해야 한다.

 

$ kubectl get pods -n kube-sample -o wide
NAME                                       READY   STATUS    RESTARTS   AGE     IP              NODE          NOMINATED NODE   READINESS GATES
nginx-toleration-fb74cd98c-hkql8           1/1     Running   0          5m31s   10.233.117.4    k1-master01   <none>           <none>

 

반응형
Posted by seungkyua@gmail.com
,
반응형

EKS 서비스를 이용하기 보다는 Kubernetes 를 AWS 에 직접 설치하여 활용하는 경우가 종종 있는데, 이 경우에는 세부적인 세팅들을 해줘야 할 때가 있다. 그 중에 대표적인 추가 모듈이 Pod 에서 EBS Volume 을 연결하여 사용할 수 있는 StorageClass 이다.

 

잘못된 Storage Class 를 사용하는 경우

Storage class 의 Provisioner 를 kubernetes.io/aws-ebs 로 사용하는 경우가 있는데, 이는 처음에는 문제가 없으나 Pod eviction (pod 가 다른 노드로 옮겨지는 경우) 에서는 문제가 발생한다. 

아래와 같은 storage class 를 적용한다.

---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: bad-storage
provisioner: kubernetes.io/aws-ebs
parameters:
  type: gp2
  fsType: ext4

 

처음 만든 Pod 에서 위의 StorageClass 를 사용하여 volume 을 ReadWriteOnce 로 attach 한 경우에는 문제없이 정상 작동한다. 하지만 pod 가 rescheduling 되면 새로 옮겨가는 노드에서 pod 는 생성되지 않는다. 이유는 volume 이 이전 노드의 Pod에 이미 사용되고 있다는 정보 때문에 새로운 노드의 Pod 에서 volume attach 가 안되기 때문이다.

즉, 해당 문제를 해결하기 위해서는 이전의 attach 정보를 삭제해 줘야 하는데 위의 storage class 로는 이를 해결할 수 없다. 그래서 다음과 같은 StroageClass 를 사용해야 한다.

 

올바른 Storage Class 사용하기

1. CSI Provisioner 설치

AWS EBS 를 위한 CSI Driver 는 다음과 같이 설치해야 한다.

$ kubectl apply -k "github.com/kubernetes-sigs/aws-ebs-csi-driver/deploy/kubernetes/overlays/stable/?ref=release-1.5"


$ kubectl get pods -n kube-system
...
ebs-csi-controller-6fd55f99c8-9j2hr                                       6/6     Running   0             13d
ebs-csi-controller-6fd55f99c8-x5dkt                                       6/6     Running   0             13d
ebs-csi-node-bw6jb                                                        3/3     Running   0             13d
ebs-csi-node-nx7gl                                                        3/3     Running   0             13d
ebs-csi-node-zh242                                                        3/3     Running   0             13d
...

 

kustomize 로 설치하면 ebs-csi-controller 가 HA 로 설치되고, 각 노드 마다 ebs-csi-node 가 설치된다. 이는 ceph 스토리지 용도의 CSI Driver 와도 비슷한데 volume 을 사용할 Pod 가 실행될 VM 노드에 EBS 링크를 연결하고 이를 Pod 에서 사용하기 때문에 각 노드마다 ebs-csi-node 가 설치되는 이유이다.

 

2. Storage class 생성

driver 를 설치했으면 이제 스토리지 클래스를 생성할 차례이다. 아래와 같이 생성한다.

$ vi standard-ebs-sc.yaml
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  annotations: 
    storageclass.kubernetes.io/is-default-class: "true"
  name: standard
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer
parameters:
  csi.storage.k8s.io/fstype: xfs
  type: io1
  iopsPerGB: "50"
  encrypted: "false"
reclaimPolicy: Delete
allowVolumeExpansion: true
allowedTopologies:
- matchLabelExpressions:
  - key: topology.ebs.csi.aws.com/zone
    values:
    - ap-northeast-2a
    - ap-northeast-2b
    - ap-northeast-2c



$ kubectl apply -f standard-ebs-sc.yaml

 

annotations 의 storageclass.kubernetes.io/is-default-class":"true" 값은 스토리지 클래스를 디폴트로 지정하는 값이다. 스토리지 클래스는 여러가지를 사용할 수 있기 때문에 기본을 지정해 주는 것이고, 또한 ebs 가 생성될 AZ 리스트를 넣어줘야 한다.

 

3. IAM 설정

앞에서 VM 에 ebs 링크를 걸어준다고 했는데 그렇기 때문에 IAM 설정을 해야 한다. 아래와 같이 Policy 를 만들고 해당 Policy 를  control-plane.cluster-api-provider-aws.sigs.k8s.io 과 nodes.cluster-api-provider-aws.sigs.k8s.io 에 attach 해야 한다.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ec2:CreateSnapshot",
                "ec2:AttachVolume",
                "ec2:DetachVolume",
                "ec2:ModifyVolume",
                "ec2:DescribeAvailabilityZones",
                "ec2:DescribeInstances",
                "ec2:DescribeSnapshots",
                "ec2:DescribeTags",
                "ec2:DescribeVolumes",
                "ec2:DescribeVolumesModifications"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:CreateTags"
            ],
            "Resource": [
                "arn:aws:ec2:*:*:volume/*",
                "arn:aws:ec2:*:*:snapshot/*"
            ],
            "Condition": {
                "StringEquals": {
                    "ec2:CreateAction": [
                        "CreateVolume",
                        "CreateSnapshot"
                    ]
                }
            }
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:DeleteTags"
            ],
            "Resource": [
                "arn:aws:ec2:*:*:volume/*",
                "arn:aws:ec2:*:*:snapshot/*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:CreateVolume"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:CreateVolume"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:CreateVolume"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:DeleteVolume"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:DeleteVolume"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:DeleteVolume"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:DeleteSnapshot"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:DeleteSnapshot"
            ],
            "Resource": "*"
        }
    ]
}

 

4. 테스트

샘플 코드를 만들어서 잘 동작하는지 테스트 해 보자.

$ vi app-ebs-example.yaml
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: app-ebs-claim
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: standard
  resources:
    requests:
      storage: 4Gi
---
apiVersion: v1
kind: Pod
metadata:
  name: app
spec:
  containers:
  - name: app
    image: centos
    command: ["/bin/sh"]
    args: ["-c", "while true; do echo $(date -u) >> /data/out.txt; sleep 5; done"]
    volumeMounts:
    - name: persistent-storage
      mountPath: /data
  volumes:
  - name: persistent-storage
    persistentVolumeCl
      claimName: app-ebs-claim



$ kubectl apply -f app-ebs-example.yaml

 

위의 코드를 설치하면 pvc 가 아래와 같이 잘 생성되는 것을 알 수 있다.

$ kubectl get pvc
NAME            STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
app-ebs-claim   Bound    pvc-4ab38fe9-7e76-4d81-85d3-0db68da9c1e0   4Gi        RWO            standard       6s


$ kubectl get volumeattachments
NAME                                                                   ATTACHER          PV                                         NODE                                             ATTACHED   AGE
csi-60f252627e620b1e91d7d32a7d463b406a3850f3a7d7b0c535fe209f699ba3bd   ebs.csi.aws.com   pvc-4ab38fe9-7e76-4d81-85d3-0db68da9c1e0   ip-10-0-214-57.ap-northeast-2.compute.internal   true       2m8s

 

또한 volumeattachments 를 보면 VM 에 해당 volume 이 attach 되어 링크가 걸려 있는 것을 알 수 있다.

 

 

반응형
Posted by seungkyua@gmail.com
,
반응형

배경

외부에서 Kubernetes Cluster 위에서 돌아가는 서비스에 접근하기 위해서는 Ingress Controller 를 통해서 가능하다. aws 와 같은 클라우드에서는 ELB -> Ingress Controller -> Workload 경로로 접근가능하다.

현재 사용하고 있는 방법은 CrossPlane 으로 AWS ELB (Classic Type) 를 자동으로 생성하여 Ingress Controller 에 연결하고 있다. 그렇기 때문에 Ingress Controller 를 배포할 때 Service Type을 특정 NodePort 를 지정/오픈하여 연결점을 알아야 한다.

예를 들면 아래와 같이 value 값을 override 해야 한다.

controller:
  replicaCount: 2
  service:
    externalTrafficPolicy: Local
    type: NodePort
    nodePorts:
      http: 32080
      https: 32443
      tcp:
        10254: 32081
  hostPort:
    enabled: true
tcp:
  10254: "10254:healthz"

 

CrossPlane 이 AWS ELB 에 대해서 Classic Type 만 지원하기 때문에 CrossPlane 을 제거하고 Network Type 으로 변경하는 것을 고민하였다. 참고로 현재의 방법은 Ingress Controller 를 실수로 삭제해도 ELB 는 그대로 남겨져 있어야 한다는 전제로 고려한 방법이다. DNS 가 ELB 와 연동되어야 하기 때문에 ELB 의 삭제 후 재생성은 DNS 전파의 시간을 필요로 하기 때문이다. 

하지만 AWS 에서 권장하는 NLB 로 넘어가는 것, NodePort 를 지정하지 않아도 된다는 것, 그리고 CrossPlane 으로 ELB를 관리하지 않아도 되기 때문에 운영 측면에서 더 낫다고 판단되어 방법을 찾아보게 되었다.

 

해결책

Kubernetes 프로젝트 아래 Ingress Controller 의 annotation 을 사용하면 NLB 를 자동으로 생성할 수 있다.

$ helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
$ helm repo update

$ helm search repo ingress-nginx/ingress-nginx

$ vi ingress-nginx-values.yaml
---
controller:
  replicaCount: 2
  affinity:
    podAntiAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        podAffinityTerm:
          labelSelector:
            matchExpressions:
            - key: app.kubernetes.io/name
              operator: In
              values:
              - ingress-nginx
          topologyKey: "kubernetes.io/hostname"
  nodeSelector:
    app.kubernetes.io/name: ingress-nginx
  service:
    annotations:
#      service.beta.kubernetes.io/aws-load-balancer-name: "ahnsk-ingress"
      service.beta.kubernetes.io/aws-load-balancer-type: "nlb"
      service.beta.kubernetes.io/aws-load-balancer-proxy-protocol: "*"
#      service.beta.kubernetes.io/aws-load-balancer-target-group-attributes: "proxy_protocol_v2.enabled=true,preserve_client_ip.enabled=true,deregistration_delay.timeout_seconds=120,deregistration_delay.connection_termination.enabled=true"
    externalTrafficPolicy: Local
    type: LoadBalancer
#    healthCheckNodePort: 32081
  config:
    enable-underscores-in-headers: "true"
    use-proxy-protocol: "true"
    proxy-body-size: "10m"

 

nodeSelector 와 podAntiAffinity 를 사용하여 가능하면 지정노드에 분포해서 Ingress Controller 를 설치할 수 있다. 

Service 의 annotations 을 사용하면 ELB 를 NLB type 으로 생성하여 자동으로 Ingress Controller 에 연결시켜 준다. 한가지 주의할 점은 aws-load-balancer-proxy-protocol 을 사용하면 aws-load-balancer-target-group-attributes 의 proxy_protocol_v2 를 enabled 해야 한다. 그러나 이 annotations 은 동작하지 않는다. 왜냐하면 aws 에서는 Ingress Controller (https://github.com/kubernetes-sigs/aws-load-balancer-controller/tree/main/helm/aws-load-balancer-controller)를 kubernetes-sigs 아래에 따로 만들어서 관리하기 때문이다. 즉 더 이상의 Nginx Ingress Controller 에 대한 기능 추가가 없다고 공표했다.

그럼에도 불구하고 Nginx Ingress Controller 를 선호한다. 이유는 Nginx 의 기능, 즉 config 를 원하는 대로 세팅할 수 있기 때문이다. 

nginx 를 사용할 때 가장 많이 경험하는 에러가 2가지 있다. 하나는 header 에 '_' 값이 포함될 때 에러가 나고 다른 하나는 body-size 로 인한 에러이다. 이 2가지 값은 기본적으로 허용해 주는 것이 좋다. 

마지막으로 ELB, Ingress Controller 를 거치면서 real client ip 를 알고 싶어 하는 경우가 많다. nginx 의 use-proxy-protocol: "true" 로 client ip 를 알아 낼 수 있다.

 

이렇게 세팅한 값으로 Ingress Controller 를 배포한다.

$ kubectl label node ip-10-0-181-43.ap-northeast-2.compute.internal app.kubernetes.io/name=ingress-nginx
$ kubectl label node ip-10-0-214-57.ap-northeast-2.compute.internal app.kubernetes.io/name=ingress-nginx

$ helm upgrade -i ingress-nginx ingress-nginx/ingress-nginx -n ingress-nginx --version 4.0.16 --create-namespace -f ingress-nginx-values.yaml

 

배포가 진행될 때 다음과 같이 NLB 의 Target Group 속성 중에 proxy_protocol_v2 값을 활성화 해줘야 한다. aws console 에서도 가능하지만 aws cli 를 통해서도 가능하다.

$ aws elbv2 describe-target-groups | jq .'TargetGroups[] | select(.VpcId == "vpc id 값") | .TargetGroupArn'

--- output ---
"arn:aws:elasticloadbalancing:ap-northeast-2:..."
"arn:aws:elasticloadbalancing:ap-northeast-2:..."

$ aws elbv2 modify-target-group-attributes --target-group-arn arn:aws:elasticloadbalancing:ap-northeast-2:... --attributes 'Key=proxy_protocol_v2.enabled,Value=true'

$ aws elbv2 modify-target-group-attributes --target-group-arn arn:aws:elasticloadbalancing:ap-northeast-2:... --attributes 'Key=proxy_protocol_v2.enabled,Value=true'

 

테스트

sample application 을 배포하여 실제 호출이 잘 되는지 확인한다. 아래는 배포 후에 http header 를 출력해 본 결과이다.

➜ ~ curl -L http://nginx-ahnsk.taco-cat.xyz
GET / HTTP/1.1
Host: nginx-ahnsk.taco-cat.xyz
X-Request-ID: 1ca37cbd4fe84f1c20e56e7ce014bd4c
X-Real-IP: 218.237.0.56
X-Forwarded-For: 218.237.0.56
X-Forwarded-Host: nginx-ahnsk.taco-cat.xyz
X-Forwarded-Port: 443
X-Forwarded-Proto: https
X-Forwarded-Scheme: https
X-Scheme: https
user-agent: curl/7.64.1
accept: */*

 

X-Real-IP 와 X-Forwared-For 에 실제 client ip 값이 출력되는 것을 확인할 수 있다.

 

마치며

Kubernetes 위에 서비스를 올린 후 사용자 접근을 오픈하기 위해서는 LB 와 Ingress Controller 를 사용해야 한다. 직접 Kubernetes 를 설치 관리하면서 Nginx 의 기능을 사용하고 싶다면 AWS Load Balancer Controller 보다는 Nginx Ingress Controller 를 활용해야 한다. 물론 IAM Account 연동 등을 위해서 혹은 EKS Cluster 를 사용한다면 AWS Load Balancer Controller 를 사용하는 것이 건강에 좋을 것이다.



반응형
Posted by seungkyua@gmail.com
,
반응형

2018년에 Kubernetes 상에서 돌아가는 ML Platform 인 Kubeflow 를 처음 알게된 후로 관심을 갖고 재미삼아 하다가 2022년에는 업무와 연관되어 일을 해야하기 때문에 중점으로 살펴보고자 한다. (최근에는 ML Platform 보다는 AI Platform 이라고 말하는 사람들이 많은 듯)

 

Kubeflow 를 KubeCon 에서 만나다

Kubeflow 개발자들을 처음 만난 것은 2019년 KubeCon in Europe (Barcelona) 이다. 초기 Kubeflow 는 Goolge 과 Arrito 가 주축이 되어 개발하고 있었는데 마침 초기 버전이 나오면서 Kubeflow 홍보를 시작하던 때 였었다.

Kubeflow 의 주 개발자는 화면에 보이는 구글의 Senior S/W Engineer 인 Jeremy Lewi 다. 당시 저 발표는 간단한 설명후에 데모 형태로 발표가 되었는데 Kubernetes 위에 Kubeflow 를 설치한 상태에서 Jupyter Notebook 을 띄어 Model 을 개발하고, Fairing 으로 Model Training 후 Model Serving 하는 것을 시연으로 보여주었다.

 

영상은 아래를 클릭하면 볼 수 있다.

https://www.youtube.com/watch?v=-GYiatVNemY 

 

저녁에 Arrito 가 주최하는 Kubeflow 개발자 / 사용자와의 저녁 만남이 있었는데 거기서 Jeremy 와 이야기를 해봤을 때는 전형적인 내성적이고 약간은 고지식한 개발자라는 느낌을 받았던 기억이(물론 이건 주관적인 생각). 시간이 좀 지나니 혼자 따로 앉아 있던데. 

 

 

Kubeflow 를 왜 써야 하나?

AI Platform Developer 라면 아래의 그림은 많이 봐왔을 것이다. 정 가운데에 있는 ML Code 는 데이터 사이언티스트들이 모델을 개발하는 코드인데, 이런 개발을 도와주거나 실제 서비스로 구축하기 위해서는 코드 이외에도 많은 Tool / System 들이 필요하다는 의미를 보여준다.  

https://papers.nips.cc/paper/5656-hidden-technical-debt-in-machine-learning-systems.pdf

 

또한 위의 동영상에서도 아래와 같이 설명하고 있다.

 

 

간단하게 살펴보는 Kubeflow Architecture

아키텍처에 대한 설명은 Kubeflow document 사이트를 참고하여 간단히 설명하고자 한다.

https://www.kubeflow.org/docs/started/architecture/

 

 

 

ML 개발 프로세스는 Experimental Phase (실험 단계) 와 Production Phase (운영 단계) 로 나눌 수 있으며 전체 워크플로우는 아래와 같다.

실험 단계

1. 문제를 인식하고 데이터를 수집/분석

2. ML 알고리즘을 선택하여 모델을 코딩

3. 데이터를 가지고 모델 트레이닝

4. 하이퍼 파라미터 튜닝

 

운영 단계

1. 데이터 변환

2. 모델 트레이닝

3. 온라인/배치 예측을 위한 모델 서빙

4. 모델 성능 측정 (결과를 가지고 다시 트레이닝하거나 튜닝)

 

 

단계에 적용되는 Kubeflow 컴포넌트는 아래와 같다. 

 

  • Fairing: 위성발사를 생각해보면 로켓 맨 상단에 중요 내용물인 위성을 보호하기 위해서 Fairing 으로 감싸고 있다. 모델을 안전하게 다른 시스템으로 전달하는 기능으로 python library 를 제공한다.
  • Pipelines: Argo workflow 기반으로 python library 를 제공하여 pipeline 을 구축할 수 있다. 예를 들면 데이터 전처리 등 반복되는 작업을 자동으로 수행할 수 있다. 
  • Katib: 하이퍼 파라미터 튜닝을 자동으로 해주는 기능이다. 아래 그림과 같이 하이퍼 파라미터를 바꾸면서 결과를 그래프와 표로 보여준다.
  • KFServing:  Kubeflow 에서 제공해주는 Model Serving 기능이다.

 

마치며

현재 Kubeflow 의 최신 버전은 v1.4 이다. 2019년 Arrito 개발자들과 만나서 논의했을 때 (당시에 v0.3 인걸로 기억한다) v1.0 으로 올리는 것이 의미있냐고 질문을 받은 적이 있다. 고객이 오픈소스의 안정성을 의심하기 때문에 기능이 조금 미흡하더라도 v1.0 으로 올리는 것이 좋겠다는 의견을 준적이 있었는데 어느덧 버전이 v1.4 까지 나왔다.(물론 그 때 이후 한참 지나서야 v1.0 이 나왔지만)

많은 기업에서 자체 AI Platform 을 만들어서 사용하는 것으로 알고 있는데, 제 개인적인 생각으로는 지금이라도 Kubeflow 로 바꾸는 것이 좋다는 생각이다. 그 이유는 이미 모두 알고 있을 것이다.

반응형
Posted by seungkyua@gmail.com
,