API 요청을 관리하는 데에는 여러 이유가 있습니다. 그 중에서도 토큰 버킷을 사용하는 이유는 다음과 같습니다:
트래픽 제어: 토큰 버킷을 사용하면 API로 들어오는 트래픽을 제어할 수 있습니다. 이를 통해 서버에 과도한 부하가 걸리는 것을 방지하고, 서비스의 안정성을 유지할 수 있습니다.
사용량 제한: 토큰 버킷을 이용하면 API를 사용하는 클라이언트의 요청 수를 제한할 수 있습니다. 이는 과도한 사용량으로 인한 서버 과부하를 방지하고, 공정한 서비스 이용을 보장합니다.
TokenBucket 만들기
토큰 버킷 패턴은 고정된 속도로 토큰을 생성하고, 요청이 들어올 때마다 토큰을 소비하여 일정량의 토큰을 가지고 있는지 확인하여 처리하는 방식입니다. 알고리즘은 아래와 같습니다.
TokenBucket은 버켓(바구니)가 있다고 가정합니다.
요청이 들어오면, 버켓에서 1개를 소비해서 그 다음의 로직을 진행시킵니다. 요청이 들어왔을 때 버킷에 토큰이 없으면 drop합니다.
고정된 속도(r)로 버킷에 토큰채웁니다.
TokenBucket 을 middle ware 에 추가
- FastAPI의 starlette을 베이스로 작성되어있어서, 이 미들웨어를 서브클레싱해서 손쉽게 token bucket을 사용할 수 있습니다.
- starlette.middleware.base.BaseHTTPMiddleware을 초기화시 app을 받게되어있고 이 베이스클레스는 dispatch라는 함수를 구현하게어 있습니다.
class BaseHTTPMiddleware:
...
async def dispatch(
self, request: Request, call_next: RequestResponseEndpoint
) -> Response:
raise NotImplementedError() # pragma: no cover
dispatch을 함수를 아래와 같이 구현해서 bucket.consume_token()이라는 함수가 bool으로 false or True을 구현하게 함에따라, 예외를 일으킬것인지 다음 로직을 실행시킬것인지를 결정하게 작성합니다.
위의 구현을 위해 아래와 같이 tokenbucket을 정의합니다.
초기화시, 버킷의 토큰 최대 저장량(max_len)과 토큰을 채우는 시간(r, refill_rate)을 지정하여 아래와 같이 작성합니다.
실행
- 다음과 같이 python3으로 fastAPI을 구동시킵니다.
(misc) (base) heon@gpusvr03:~/repositories/misc/tokenbucket_ex$ python3 app.py
INFO: Started server process [4121818]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:30000 (Press CTRL+C to quit)
그 후 요청 15개를 보내봅니다. 아래와 같이 10개를 토큰을 다 소비한경우 5개의 요청이 예외처리됩니다.
import mlflow
from setting import parent_run_id
mlflow.set_tracking_uri(TRACKING_URI)
child_run_ids = list()
for run in client.search_runs(experiment_ids=[6]):
if "mlflow.parentRunId" not in run.data.tags:
continue
if run.data.tags["mlflow.parentRunId"] != parent_run_id:
continue
child_run_ids.append(run.info.run_id)
AWS Identity and Access Management (IAM)은 Amazon Web Services (AWS)에서 제공하는 서비스로, AWS 리소스에 대한 액세스를 관리하고 보안을 강화하는 데 사용됩니다. IAM을 사용하여 AWS 계정에 사용자, 그룹 및 역할을 생성하고, 이러한 엔터티에 대한 권한을 정의하여 AWS 리소스에 대한 액세스를 제어할 수 있습니다. 본 과정에서는 github action으로 AWS 리소스(ECR, EC2)을 제어 하기 위함입니다.
3. Github secrets 설정: 리포지토리 내, Settings-Security 내 Secrets and variable - Actions 등록
Action secrets는 GitHub Actions 워크플로우에서 사용되는 민감한 정보를 안전하게 저장하고 관리하기 위한 기능입니다. Secrets는 암호화되어 안전한 저장소에 저장되며, 워크플로우 실행 시에만 필요한 환경 변수로 사용할 수 있습니다. Action에서 사용하게되는 변수들을 repository에서 안전하게 저장하고, action에서 사용하는 yaml파일에서 접근이 가능합니다.
다음과 같이 등록이 필요합니다.
AWS_ACCESS_KEY_ID: AWS(Amazon Web Services) 계정에 액세스하는 데 사용되는 액세스 키의 ID 부분입니다. AWS API에 액세스할 때 사용
AWS_SECRET_ACCESS_KEY: AWS(Amazon Web Services) 계정에 액세스하는 데 사용되는 액세스 키의 시크릿 키 부분입니다. AWS API에 액세스할 때 사용
AWS_REGION: AWS 리소스가 위치한 지역(region)을 지정하는 변수. 참고로 ap-northeast-2=아시아 태평양(서울)입니다.
REPO_NAME: ECR 리포지토리에 접근하는 데 사용되는 계정 ID 또는 별칭입니다. ECR에 도커 이미지를 푸시하거나 불러올 때 사용
EC2_PRIVATE_KEY: AWS EC2 인스턴스에 SSH로 접속할 때 사용되는 개인 키(private key). EC2의 pem키의 내용을 그대로 copy & paste하시면 됩니다. "--benign", "--end"을 포함하여 plain text로 복붙하시면됩니다!
4. ECR 생성하기: dockerhub을 이용하셨다면, 같은 기능을 하는 AWS의 dockerhub라고 보셔도 무관합니다.
AWS 검색화면에서 ECR을 검색하여 다음과 같이, 리포지토리(repository)을 생성합니다. 아래와 같이 private으로 설정할 수 있습니다. 그리고, Repository name에보면, "숫자".dkr.ecr.ap-northeast-2.amazonaws.com 으로 나열되는데 "숫자"에 해당하는 부분이 aws_account_id이고, ap-northeast-2가 region입니다[1]. aws_account_id도 secret_key로 본 포스팅에서는 등록하였습니다.
1. Action yaml파일 생성: github/workflows 디렉토리에 새 워크플로우 파일을 생성합니다. 예를 들어, deploy.yml 파일을 만들 수 있습니다. 아래와 같이 "set up a workflow yourself"을 눌러 workflow(yaml파일)을 처음부터 작성할 수 있습니다.
그리고, 아래와 같이 yaml파일의 초반부를 작성합니다.
name: Deploy to EC2 with ECR
on:
push:
branches:
- main # 배포할 브랜치를 선택합니다. 원하는 브랜치로 변경 가능
env:
EC2_USER: ubuntu
EC2_HOST: <EC2의 IPv4 공개IP>
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
AWS 자격증명: AWS 액세스 키와 시크릿 키, 그리고 지정한 AWS 리전을 사용하여 AWS 자격증명을 설정합니다.
Docker 이미지 ECR로 푸시: 이미지를 ECR로 푸시하기 위해 해당 이미지를 태깅하고, ECR에 로그인한 뒤 이미지를 푸시합니다. 위의 단계에서 만든 "my-image:latest"의 이미지를 ECR로 푸시하기위해서, ECR에 로그인하고, ECR리포지토리에 맞게 테그명을 변경합니다. 만일 ECR의 URL가 "000000000.dkr.ecr.ap-northeast-2.amazonaws.com/my-image" 였다면, 이미지도 재 태그하여 "000000000.dkr.ecr.ap-northeast-2.amazonaws.com/my-image:<버전테그>" 형식어야합니다. 즉 "/????"의 이름도 같아야합니다.
EC2 인스턴스로 SSH 접속: EC2 인스턴스에 접속하기위해서 ".pem"파일의 내용을 sercrets 변수로부터 stdout을 "key.pem"을 작성을 합니다. 이를 이용하여 ssh -i <pem파일경로> 로 접속합니다. 그리고, SSH에 접속후에 ECR로부터 이미지를 pull 받고, 이미 띄워져있는 컨테이너가있다면 중단, 삭제하고, 새로운 이미지를 컨테이너로 구동합니다. 그리고 마지막으로 pem키를 삭제합니다.
master node에 쿠버네티스를 세팅하고, 각 노드랑 연결해보려고합니다. 쿠버네티스로 클러스터를 구성하는 것은 1) control-plane을 설치해야합니다(단일 control-plane또는 복수의 control-plane도 가능), 2) 또한, 각 워커노드들을 등로갛여 파드들의 네트웍이 클러스터내에서 잘 통신되게 하는 것입니다.
시작전에 앞서
kubeadm을 이용해서 쿠버네티스를 세팅해보려고합니다. kubeadm으로 쿠버네티스를 세팅하는 것이 쿠버네티스 공식홈페이지에서도 "best practice"라고 설명되어있습니다. kubeadm은 쿠버네티스를 간단히 설치해서 사용해보기에 용이합니다.
사용 전 확인사항
쿠버네티스를 사용하기전에 장치에 아래의 스펙요구사항을 만족하는지 확인해야합니다.
A compatible Linux host. The Kubernetes project provides generic instructions for Linux distributions based on Debian and Red Hat, and those distributions without a package manager.
2 GB or more of RAM per machine (any less will leave little room for your apps).
2 CPUs or more.
Full network connectivity between all machines in the cluster (public or private network is fine).
Unique hostname, MAC address, and product_uuid for every node. Seeherefor more details.
Certain ports are open on your machines. Seeherefor more details.
Swap disabled. YouMUSTdisable swap in order for the kubelet to work properly
마지막의 swap을 끄라고 되어있는데 이는 "kubelet"이라는 데몬이 정상적으로 돌게하기위함이라고 합니다. "kubelet"은 파드에서 컨테이너들이 "선언적으로 정의한"(=내가 세팅한 그대로 현 상태를 유지해주는 것)대로 실행되게끔 해주는 데몬입니다.
본 포스팅에서는 카카오클라우드를 이용해서 private network로 구성된 클러스터를 구성해볼 예정입니다. 사설 IP로 구성된 노드들은 아래와 같습니다.
MAC address가 유요한지 product uuid가 각 노드에서 유니크한지 확인합니다. 아래의 명령어를 이용해서, 각 노드에서 MAC adresss, product_uuid가 유일한지 확인해봅니다. 일단은 마스터노드만 확인해봅니다.
ubuntu@k8s-master:~$ ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/loopback [.........]
2: ens3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000
link/ether [..........]
ubuntu@k8s-master:~$ sudo cat /sys/class/dmi/id/product_uuid
[...........]
K8S을 설치하기전에 필수로 설치해야할 것이 docer가 있습니다. Docker은 다음의 링크로 설치하시길바랍니다 [1]
kubeadm, kubelet, kubectl을 설치할 것입니다. 각 기능은 아래와같습니다.
kubeadm: 클러스터를 구축하고 실해나는 과정을 의미합니다 (=cluster bootstrap). control-plane, worker nodes을 구성하고 각 노드들끼리 올바른 정보들을 가지고있는지, 통신이 한지를 확인합니다. [2]
kubelet: 선언적으로 정의한 대로 파드를 시작하거나, 컨테이너를 시작해주는 기능을 합니다.
[addons] Applied essential addon: CoreDNS
[addons] Applied essential addon: kube-proxy
Your Kubernetes control-plane has initialized successfully!
To start using your cluster, you need to run the following as a regular user:
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
Alternatively, if you are the root user, you can run:
export KUBECONFIG=/etc/kubernetes/admin.conf
You should now deploy a pod network to the cluster.
Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at:
https://kubernetes.io/docs/concepts/cluster-administration/addons/
Then you can join any number of worker nodes by running the following on each as root:
# 하단의 명령어는 적을 수 있다면 잘 기록하시길 바랍니다.
kubeadm join 172.16.2.231:6443 --token y24p4r.7itfeqy96rkjof8q \
--discovery-token-ca-cert-hash sha256:5e9e43eadba4eb7858033de6fbb4d99cc811f7d49d96f3637435370463c087e
클러스터의 실행을 위해서 위의 나온 메시지대로 $HOME/.kube/config 파일을 설정합니다. (일반사용자로 전환후)
// 명렁어로 토큰 출력
# kubeadm token list
// 아래의 명령어로 해시 출력
# openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | openssl rsa -pubin -outform der 2>/dev/null | openssl dgst -sha256 -hex | sed 's/^.* //'
control-plane에서만 deployment을 실행하면 파드가 생성되지않음을 확인할 수 있습니다. deployment에는 replicas 수를 3으로 지정해놓았는데요. 할당할 worker노드가 없기 때문에 pending으로 나온 것으로 판단됩니다. 실제로 하나의 파드의 상태를 추적해보면 아래와 같습니다.
root@k8s-master:/home/ubuntu# kubectl apply -f controllers-nginx-deployment-yaml
deployment.apps/nginx-deployment created
# watch "kubectl get pods"
Every 2.0s: kubectl get pods k8s-master: Sun Jan 8 09:00:15 2023
NAME READY STATUS RESTARTS AGE
nginx-deployment-85996f8dbd-98xlx 0/1 Pending 0 118s
nginx-deployment-85996f8dbd-d9bhg 0/1 Pending 0 118s
nginx-deployment-85996f8dbd-kjk8t 0/1 Pending 0 118s
root@k8s-master:/home/ubuntu# kubectl describe pod nginx-deployment-85996f8dbd-98xlx
Name: nginx-deployment-85996f8dbd-98xlx
Namespace: default
Priority: 0
Service Account: default
Node: <none>
Labels: app=nginx
pod-template-hash=85996f8dbd
Annotations: <none>
Status: Pending
IP:
IPs: <none>
Controlled By: ReplicaSet/nginx-deployment-85996f8dbd
Containers:
nginx:
Image: nginx:1.14.2
Port: 80/TCP
Host Port: 0/TCP
Environment: <none>
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-h2w8s (ro)
Conditions:
Type Status
PodScheduled False
Volumes:
kube-api-access-h2w8s:
Type: Projected (a volume that contains injected data from multiple sources)
TokenExpirationSeconds: 3607
ConfigMapName: kube-root-ca.crt
ConfigMapOptional: <nil>
DownwardAPI: true
QoS Class: BestEffort
Node-Selectors: <none>
Tolerations: node.kubernetes.io/not-ready:NoExecute op=Exists for 300s
node.kubernetes.io/unreachable:NoExecute op=Exists for 300s
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 49s default-scheduler 0/1 nodes are available: 1 node(s) had untolerated taint {node-role.kubernetes.io/control-plane: }. preemption: 0/1 nodes are available: 1 Preemption is not helpful for scheduling..
root@k8s-test:/home/ubuntu# kubeadm join 172.16.2.231:6443 --token 8yxfot.ktr4irill5hqw6h7 --discovery-token-ca-cert-hash sha256:bd91d60981b5774add11106cd42e1f8d6db18b241b5fd529b0d638e713522494
[preflight] Running pre-flight checks
[preflight] Reading configuration from the cluster...
[preflight] FYI: You can look at this config file with 'kubectl -n kube-system get cm kubeadm-config -o yaml'
[kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"
[kubelet-start] Writing kubelet environment file with flags to file "/var/lib/kubelet/kubeadm-flags.env"
[kubelet-start] Starting the kubelet
[kubelet-start] Waiting for the kubelet to perform the TLS Bootstrap...
This node has joined the cluster:
* Certificate signing request was sent to apiserver and a response was received.
* The Kubelet was informed of the new secure connection details.
Run 'kubectl get nodes' on the control-plane to see this node join the cluster.
control-plane (master node)로 돌아가, 파드가 정상적으로 실행되는지 확인합니다. nginx 서버 3개가 정상적으로 돌아가는 것을 확인할 수 있었습니다.
root@k8s-test:/home/ubuntu# curl localhost:30052
<!DOCTYPE html>
<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>
localhost말고, 외부에서 해당 노드로도 요청을 보내봅니다.
// 마스터노드에서 노드1의 사설IP:30052포트로 요청
ubuntu@k8s-master:~$ curl 172.16.2.253:30052
<!DOCTYPE html>
<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>
0. PUSH할 원격저장소를 만든다. docker hub (https://hub.docker.com/) 에서 로그인 후, 원격저장소를 생성한다. github저장소처럼 "ID/원격저장소명"으로 주소가 만들어진다.
1. 도커 로그인
$ docker login
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username: [YourID]
Password: ******
Login Succeeded
2. Docker hub에 올릴 이미지를 찾는다. 여기서 필자는 REPOSITOY에 simple-nlp-app, TAG은 v1을 올릴 것이다. 그렇지만, 도커허브에 올릴 때는 ID/repository명 = docker image ls에서의 REPOSITORY 명이 동일해야한다. 따라서, 한번 변경이 필요하다.
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
simple-nlp-app v1 7108c8dbe574 6 minutes ago 2.73GB
simple-nlp-app v0 c82b9d4786fe 9 minutes ago 2.73GB
simple-nlp-app latest 798c7cf60156 2 days ago 2.72GB
nginx latest ac8efec875ce 6 days ago 142MB
alpine/git latest 22d84a66cda4 3 weeks ago 43.6MB
hubproxy.docker.internal:5000/docker/desktop-kubernetes kubernetes-v1.25.2-cni-v1.1.1-critools-v1.24.2-cri-dockerd-v0.2.5-1-debian 09d7e1dbc2c4 2 months ago 363MB
k8s.gcr.io/kube-apiserver v1.25.2 97801f839490 2 months ago 128MB
k8s.gcr.io/kube-controller-manager v1.25.2 dbfceb93c69b 2 months ago 117MB
k8s.gcr.io/kube-scheduler v1.25.2 ca0ea1ee3cfd 2 months ago 50.6MB
k8s.gcr.io/kube-proxy v1.25.2 1c7d8c51823b 2 months ago 61.7MB
k8s.gcr.io/pause 3.8 4873874c08ef 5 months ago 711kB
k8s.gcr.io/etcd 3.5.4-0 a8a176a5d5d6 6 months ago 300MB
k8s.gcr.io/coredns v1.9.3 5185b96f0bec 6 months ago 48.8MB
docker/desktop-vpnkit-controller v2.0 8c2c38aa676e 19 months ago 21MB
docker/desktop-storage-provisioner v2.0 99f89471f470 19 months ago 41.9MB
nginx 1.14.2 295c7be07902 3 years ago 109MB
dattarajrao/simple-app latest 3f4a466b587c 4 years ago 132M
3. 두 가지 방법이 있는데, 첫 번째는 docker image tag을 이용하는 방법.
docker image tag SOURCE_IMAGE[:TAG] TARGET[:TAG] . 이 명령어는 원본 이미지에서 타겟이이지로 리포지토리 명을 다시 테그를 달 수 있게한다.
docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]] : 이 명령어는 실행중인 컨테이너를 다시 이미지화하는 방법이다.
아래의 예시는 docker image tag을 이용한 방법이다. tag을 이용할 때, 미리 생성한 원격저장소의 이름과 동일하게 해준다. 그렇지 않으면 아래와 같이 에러가 뜬다.
$ docker push 4pymgalion/simple-nlp-app:v1
The push refers to repository [docker.io/4pymgalion/simple-nlp-app]
An image does not exist locally with the tag
따라서, 아래와 같이 원격저장소의 ID/저장소명과 같이 도커 이미지를 테그를 다시 달아준다. 그러면 추가로 "4pygmalion/simple-nlp-app"라는 리포지토리 이름을 가진 이미지가 생성된다.
$ docker tag simple-nlp-app:v1 4pygmalion/simple-nlp-app:latest
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
4pygmalion/simple-nlp-app latest 7108c8dbe574 14 minutes ago 2.73GB
simple-nlp-app v1 7108c8dbe574 14 minutes ago 2.73GB
nginx latest ac8efec875ce 6 days ago 142MB
alpine/git latest 22d84a66cda4 3 weeks ago 43.6MB
hubproxy.docker.internal:5000/docker/desktop-kubernetes kubernetes-v1.25.2-cni-v1.1.1-critools-v1.24.2-cri-dockerd-v0.2.5-1-debian 09d7e1dbc2c4 2 months ago 363MB
k8s.gcr.io/kube-apiserver v1.25.2 97801f839490 2 months ago 128MB
k8s.gcr.io/kube-scheduler v1.25.2 ca0ea1ee3cfd 2 months ago 50.6MB
쿠버네티스를 이용하면,파드를 통해 클라이언트들이 어플리케이션을 호출할 수 있다. 그러나, 클라이언트 입장에서는 각각 파드의 주소가 존재하는하고, 클라이언트는 어느 파드로 갈지 명시하지 않았는데도, 쿠버네티스가 알아서 각각의 파드가 처리하게 만들어준다. 즉, 네트워킹이 쿠버네티스에 의해서, 알아서 처리되는데, 이 네트워크 서비스를 제공할 수 있는 자원이 "서비스(Service)"이다. 즉, 파드로 실행중인 앱을 네트워크서비스로 노출하게 해주는 자원이다.
서비스를 이용하기위해 각 파드, 노드가 만족해야할 사항[1]
각 파드는 노드상의 모든 파드와 통신할 수 있어야함
노드상의 에이전트(데몬, kubelet)은 해당 노드의 모든 파드와 통신할 수 있어야함.
어플리케이션을 네트워크 서비스로 외부에 노출하기
아래와 같이, 디플로이먼트(deployment)와 서비스(Service)를 명시한 yaml파일을 각각 만든다.
그리고, 아래와 같이 클러스터 내부에서는 서비스가 가능하고, 각 클러스터의 IP을 확인할 수 있다.
$ kubectl get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 28h
my-nginx ClusterIP 10.108.171.235 <none> 80/TCP 26h
해당 클러스터의 내부
Trouble shooting: Cluster IP에 접속이 안될 때 -> Node Port로 외부 클라이언트가 접속할 수 있도록 변경 [2]
서비스하는 클러스터의 IP는 가상의 IP이기 때문에, 클러스터 내부에서 내부로만 서비스할 수 있다. 즉 위의 예시에서처럼 클러스터 IP가 10.108.171.235면, 이 클러스터로 들어가야만 "10.108.171.235:80"의 서비스를 요청하고 받을 수 있는 것이다. 이것을 해결하기, "Cluster IP"을 "Node Port"로 변경해주어야 한다.
Node Port은 외부 클라이언트가 노드로 접속하기 위한 포트를 의미한다. 이 노드 포트을 변경하려면 아래와 같이 kubectl expose을 이용한다.
expose을 하면 TYPE이 NodePort인 서비스가 하나 생성이된다. example-serivce라는 이름을 가진 서비스의 TYPE이 NodePort인 것을 잘보자. 이 클러스터의 가상 IP은 10.101.151.252이다. 이것은 여전히 가상IP이기 때문에 서비스를 요청할 수 없다. 다만, 이 노드의 원래주소(localhost, 또는 privateIP, publicIP)로 들어갈때, 30384포트로 요청을하면 알아서 80번으로 바꿔준다. 즉 포트포워딩으로 30384을 80으로 바꿔서 10.101.151.252로 요청해볼 수 있다.
$ kubectl get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
example-service NodePort 10.101.151.252 <none> 80:30384/TCP 26h
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 28h
$ curl localhost:30384
<!DOCTYPE html>
<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>
전통적인 소프트웨어 개발방식과 MLOps와의 차이: 데이터와 코드를 둘다 다뤄야하는 ML문제
ML을 이용한 서비스가 점차적으로 확대될 것이라는 것에 이 챕터의 내용이 강조되고 있다. 여태까지 소프트웨어의 발전은 보통 "개발업무(development)"라는 것으로 취급되어서, Waterfall 방식, Agile 방식, DevOps방식으로 쭉 진행되어왔다. 소프트웨어의 개발이 더 큰 개념이기 때문에, 열거한 3가지 방식중에 최근 방식인 Agile/DevOps로 해결하면 되는것이 아닌가? 생각을 하게된다. 하지만, 현업에서는 많은 실패사례들이 존재한다. 왜 그럴까? 이 책에서는 ML을 이용한 서비스가 실패하는 이유를 말하는데, 전통적인 소프트웨어 개발과의 차이점을 강조한다.
Machine Learning = data + code
DevOps라고하면, 지속적으로 코드를 개발하고, 업데이트하고, 사용자가 일부 개발과정을 포함해서 지속적으로 개발/도입/운영할 수 있는 개발방식으로 여겨지는데, MLOps도 이와 같이 했을 경우는 코드는 업데이트되지만, 데이터는 업데이트 안된다는 것에 단점이 있다. 따라서, 데이터와 코드가 지속적으로 함께 관리되어야함을 강조한다. 예) 모델은 최신모델인데 과거데이터를 쓰거나, 현재 데이터만을 이용해서 향후에 서비스 방식이 바뀌면 재도입이 어려운 것들에 대한 문제가 이해 해당된다. 이러한 단점을 줄이기 위해서 MLOps은 체계적인 방식으로 "data"와 "code(model)"을 같이 운영하는 것을 목표로한다. 이를 위해서, 모델의 개발, 도입(depolyment), 모니터링등알 함께 지속적인 방식으로 도입하는 것을 의미한다.
MLOps의 워크플로우: 어떤방식으로 진행이되나?
Figure 2은 MLOps의 워크플로우의 개념도이다. 두 형태로 나뉘는데, 상위(Build, Delopy, Monitor)은 주로 MLOps pipeline이라고 불린다.한편 하위는 Driver(드라이버)라고 불린다.
MLOps pipeline: 모델 개발, 적용, 적용후 모니터링에 대한 업무의 개요에 해당된다.
Driver: MLOps pipeline을 하기위해서 실제로 행해야하는 하위 테스크, 구성되어야하는 인프라 등을 일컫는다.
MLOps업무를 해본사람이면 대부분 이해할 수 있는 개념이다. 참고로, model packaging, model registering, application testing 에 관한 내용만 추가로 아래와 같이 기술해 놓았다.
Model packaging: 모델을 직렬화하고, 독립적인 소프트웨어로 작동할 수 있게끔 컨테이너화하는 단계를 의미한다. 주로 ONNX file로 모델을 직렬화한다. ML모델을 Training/Testing으로 나누고 실제 잘 개발되었다고하자. 더 이상 이 모델은 현시점에서 바뀔일이 없다. 따라서, Pickling을 하든, ONNX로 변경하든 직렬화해놓고, 메모리에 로드하여 바로 쓸 수 있는 단계로 만드는 일에 해당된다. 그리고, 이 직렬화한 모델을 독립적인 소프트웨어로 쓸 수 있도록, Docker등으로 필요한 데이터, 종속코드 등을 하나로 묶어 만들어놓는 것을 의미한다.
Model registering: 모델레지스터링은 직렬화+패키징된 모델을 DB등과 같은 레지스트리에 등록/저장해놓는 것을 의미한다. 이렇게 해놓으면, 언제든지 해당 모델이 필요할 때, 다운로드 받아서 쓸 수 있다.
Application testing: 동의어로 Pre-production testing이라고 생각하면된다. 모델을 개발해놓고, 실제 운영에 쓰기전에 진짜 쓸만한것인지를 Extra validation하는 것과 같다. API나 쿠버네티스, 컨테이너로 띄워놓고 훈련용이 아닌, 추가적인 테스트용도로 데이터를 보내보고, 모델이 기대하는 것과 잘 동작하는지를 의미한다. Application testing의 결과는 도메인 전문가가 직접 결과를 분석하기도 한다(실제로 잘되었는지 확인해야하니까..), 도메인 전문가가 쓸만하다라고 판단해준 이후에나 실제 개발환경에 넣고, 추론할 수 있는 대기상태로 만들어두는 것이다.
MLOps 파이프라인의 예시: 희귀질환 환자의 유전변이추천시스템을 개발했다고 하자.
이 개발할 때 썼던 데이터는 2021년 이전의 데이터였다. 그렇게 만들어낸 Top 5 recall이 96.3%였다. 여기까지가 Model build이다.
이 모델을 바로 쓸 수 있도록, ONNX으로 직렬화하고 docker image로 만들어 내었다(model packaging).
이렇게 모델패키지한 것을 도커 이미지 리포지토리에 올려놓았다(model registering, 완벽한의미의 regstering은 아님).
그리고 나서, 임상유전학자들이 판독에 쓰기직전에 추가적으로 2022년 이후의 데이터를 모아놓고, 추천시스템 API을 띄워놓은 상태에서 모아놓은 데이터를 추론해보았다. 2022년 이후의 데이터를 추론했을 때의 결과가 20개중에, top 3가 95%이상이었다. 이 결과를 임상유전학자에게 다시 보냈다(Application testing).
그 후에 model production에 쓸 수 있도록 운영 서버로부터 데이터수 있도록 Restful API을 오픈했다.