이미지 인식 AI 모델(ViT) 테스트

|

실행 환경

맥북(M1 Pro) 기반으로 실습 테스트

패키지 설치

# 가상환경 생성
python -m venv venv
source venv/bin/activate

# 패키지 설치
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
pip install transformers pillow

사용 가능 디바이스 확인

Apple Silicon 환경에서는 mps 디바이스 사용 가능

import torch

if torch.backends.mps.is_available():
    device = torch.device("mps")   # Apple GPU
elif torch.cuda.is_available():
    device = torch.device("cuda")
else:
    device = torch.device("cpu")

print("Using device:", device)

ViT 기반 모델

Hugging Face의 google/vit-base-patch16-224는 ImageNet으로 사전학습된 표준 ViT로, 맥북에서도 작은 배치로 실행 가능한 모델임. 만약 더 가벼운 모델이 필요할 경우에는 google/vit-small-patch16-224Distilled ViT 계열도 선택 가능함

main.py

아래는 input.jpg 파일을 읽어서 어떤 그림인지 키워드를 리턴해주는 코드

import torch
from PIL import Image
from transformers import AutoImageProcessor, ViTForImageClassification

# 1) 디바이스 선택 (위에서 설명한 로직 재사용 가능)
if torch.backends.mps.is_available():
    device = torch.device("mps")
elif torch.cuda.is_available():
    device = torch.device("cuda")
else:
    device = torch.device("cpu")
print("Using device:", device)

# 2) 모델 이름 선택 (맥북에서도 무난한 베이스 모델)
model_name = "google/vit-base-patch16-224"

# 3) 이미지 전처리기와 모델 로드
image_processor = AutoImageProcessor.from_pretrained(model_name)
model = ViTForImageClassification.from_pretrained(model_name).to(device)
model.eval()

# 4) 이미지 로드 (input.jpg 경로에 이미지 준비)
image_path = "input.jpg"
image = Image.open(image_path).convert("RGB")

# 5) 전처리 및 텐서 변환
inputs = image_processor(image, return_tensors="pt")
inputs = {k: v.to(device) for k, v in inputs.items()}

# 6) 추론
with torch.no_grad():
    outputs = model(**inputs)
    logits = outputs.logits
    probs = logits.softmax(dim=-1)
    top_prob, top_idx = probs.max(dim=-1)

# 7) 결과 해석
predicted_label_idx = top_idx.item()
predicted_prob = top_prob.item()
predicted_label_name = model.config.id2label[predicted_label_idx]

print(f"Predicted class: {predicted_label_name} (p={predicted_prob:.4f})")

실행 결과

Using device: mps
Fast image processor class <class 'transformers.models.vit.image_processing_vit_fast.ViTImageProcessorFast'> is available for this model. Using slow image processor class. To use the fast image processor class set `use_fast=True`.
Predicted class: banana (p=0.9748)

ReplicaSet 및 Deployment

|

ReplicaSet

지정한 개수만큼 Pod를 복제하는 리소스

snowdeer-replicaset.yaml

apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: snowdeer-http-server
  labels:
    app: snowdeer-http-server
spec:
  replicas: 3
  selector:
    matchLabels:
      app: snowdeer-http-server
  template:
    metadata:
      labels:
        app: snowdeer-http-server
    spec:
      containers:
        - name: nginx
          image: nginx:latest
$ kubectl get pods                         

NAME                         READY   STATUS    RESTARTS   AGE
snowdeer-http-server-45gsp   1/1     Running   0          14m
snowdeer-http-server-5tk4z   1/1     Running   0          14m
snowdeer-http-server-st74m   1/1     Running   0          14m
$ kubectl get replicaset snowdeer-http-server

NAME                   DESIRED   CURRENT   READY   AGE
snowdeer-http-server   3         3         3       14m

Deployment

하지만, 본격적 운영 환경에서는 ReplicaSet을 추천하지는 않고 Deployment를 권장함. Deployment를 사용하는 가장 큰 이유는 무중단 업데이트 기능.

snowdeer-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: snowdeer-nginx-deployment
  labels:
    app: snowdeer-nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: snowdeer-nginx
  template:
    metadata:
      labels:
        app: snowdeer-nginx
    spec:
      containers:
        - name: nginx
          image: nginx:1.25.3
          ports:
            - containerPort: 80

실행

$ kubectl apply -f snowdeer-deployment.yaml 

deployment.apps/snowdeer-nginx-deployment created

$ kubectl get deployment

NAME                        READY   UP-TO-DATE   AVAILABLE   AGE
snowdeer-nginx-deployment   3/3     3            3           48s

$ kubectl get pods      

NAME                                         READY   STATUS    RESTARTS   AGE
snowdeer-nginx-deployment-847759b688-2zbgh   1/1     Running   0          47s
snowdeer-nginx-deployment-847759b688-j7pql   1/1     Running   0          44s
snowdeer-nginx-deployment-847759b688-nl6cw   1/1     Running   0          41s

$ kubectl get replicaset                     

NAME                                   DESIRED   CURRENT   READY   AGE
snowdeer-nginx-deployment-847759b688   3         3         3       76s

무중단 업데이트 실습

아까 작성했던 snowdeer-deployment.yaml 파일을 수정해서 nginx의 버전을 수정함

snowdeer-deployment.yaml(수정)

컨테이너 이미지를 수정함

apiVersion: apps/v1
kind: Deployment
metadata:
  name: snowdeer-nginx-deployment
  labels:
    app: snowdeer-nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: snowdeer-nginx
  template:
    metadata:
      labels:
        app: snowdeer-nginx
    spec:
      containers:
        - name: nginx
          image: nginx:latest
          ports:
            - containerPort: 80

실행

$ kubectl apply -f snowdeer-deployment.yaml 

deployment.apps/snowdeer-nginx-deployment configured

$ kubectl get pods      

NAME                                         READY   STATUS    RESTARTS   AGE
snowdeer-nginx-deployment-847759b688-gzztg   1/1     Running   0          15s
snowdeer-nginx-deployment-847759b688-xc75g   1/1     Running   0          18s
snowdeer-nginx-deployment-847759b688-zszxt   1/1     Running   0          20s

$ kubectl get replicaset                     

NAME                                   DESIRED   CURRENT   READY   AGE
snowdeer-nginx-deployment-66fb57596b   0         0         0       2m18s
snowdeer-nginx-deployment-847759b688   3         3         3       4m59s

Deployment 업데이트 방식

Deployment의 업데이트 방식은 기본적으로 RollingUpdate로 되어있음. 아래 명령어로 확인 가능

$ kubectl describe deployment snowdeer-nginx-deployment

Name:                   snowdeer-nginx-deployment
Namespace:              default
CreationTimestamp:      Sun, 07 Dec 2025 22:11:52 +0900
Labels:                 app=snowdeer-nginx
Annotations:            deployment.kubernetes.io/revision: 4
Selector:               app=snowdeer-nginx
Replicas:               3 desired | 3 updated | 3 total | 3 available | 0 unavailable
StrategyType:           RollingUpdate
MinReadySeconds:        0
RollingUpdateStrategy:  25% max unavailable, 25% max surge
// ...

RollingUpdateStrategy: 25% max unavailable, 25% max surge 항목은 전체 25%의 Pod까지 동시 종료할 수 있음을 의미함. max surge는 최대 몇 개의 Pod를 새로 생성할 수 있는것인지를 의미하며, 오래된 Pod와 신규 Pod로 인해 필요 클러스터의 용량과 비용 증가 가능성이 있기 때문에 주의해서 설정할 필요 있음.

하지만 경우(빠르게 전체 적용 등)에 따라서는 Recreate 타입이 더 유리한 경우도 있음.

그런 경우에는 아래와 같이 선언 가능

snowdeer-deployment.yaml(수정)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: snowdeer-nginx-deployment
  labels:
    app: snowdeer-nginx
spec:
  replicas: 3
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: snowdeer-nginx
  template:
    metadata:
      labels:
        app: snowdeer-nginx
    spec:
      containers:
        - name: nginx
          image: nginx:latest
          ports:
            - containerPort: 80

kubectl 명령어로 디버깅 해보기

|

사이드카 컨테이너

k8s 1.25부터 Stable된 명령어로 컨테이너에 경량화나 보안을 위해 Shell 조차 없을 경우 사용가능한 방법

kubectl debug --stdin --tty [디버그대상Pod명] --image=[디버그용컨테이너이미지] --target=[디버그대상컨테이너명]

현재 아래와 같이 컨테이너가 동작 중

$ kubectl get pods                                 

NAME           READY   STATUS    RESTARTS   AGE
snowdeer-app   1/1     Running   0          5h11m

실행

$ kubectl debug --stdin --tty snowdeer-app --image=curlimages/curl:latest --target=hello-server -- sh

Targeting container "hello-server". If you don't see processes from this container it may be because the container runtime doesn't support this feature.
Defaulting debug container name to debugger-55kh7.
If you don't see a command prompt, try pressing enter.
~ $ 

여기에 curl localhost:8080 입력 하면 아래와 같이 결과가 나옴

$ kubectl debug --stdin --tty snowdeer-app --image=curlimages/curl:latest --target=hello-server -- sh

Targeting container "hello-server". If you don't see processes from this container it may be because the container runtime doesn't support this feature.
Defaulting debug container name to debugger-55kh7.
If you don't see a command prompt, try pressing enter.
~ $ curl localhost:8080
Hello, world!~ $ 
~ $ 

--stdin--tty 대신 -it를 쓰면 좀 더 간편해짐

$ kubectl debug -it snowdeer-app --image=curlimages/curl:latest --target=hello-server -- sh

kubectl exec

kubectl exec 명령어로 컨테이너에 직접 접속 가능

예시로 curl Pod를 하나 생성

$ kubectl run snowdeer-curl --image=curlimages/curl:latest --command -- /bin/sh -c "while true; do sleep infinity; done;"

pod/snowdeer-curl created

$ kubectl get pods -o wide     

NAME            READY   STATUS    RESTARTS   AGE     IP           NODE                             NOMINATED NODE   READINESS GATES
snowdeer-app    1/1     Running   0          5h20m   10.244.0.5   snowdeer-cluster-control-plane              
snowdeer-curl   1/1     Running   0          2m22s   10.244.0.7   snowdeer-cluster-control-plane              
</pre>

## 접속

$ kubectl exec -it snowdeer-curl -- /bin/sh                         

# 위에서 확인했던 snowdeer-app의 주소로 명령어를 날려봄
~ $ curl 10.244.0.5:8080
Hello, world!~ $ 
# 포트포워딩을 통한 접속 k8s의 Pod에는 쿠버네티스 내부의 IP 주소가 할당됨. 즉, 외부에서는 기본적으로 접속 불가능. `Service`를 이용해서 접속하는 것이 일반적이나 `port-forward` 명령어를 이용해서 접근 가능 아래 명령어로 로컬PC의 `3000`번 포트를 `snowdeer-app`의 `8080`에 연결할 수 있음
$ kubectl port-forward snowdeer-app 3000:8080  

Forwarding from 127.0.0.1:3000 -> 8080
Forwarding from [::1]:3000 -> 8080
그 이후 로컬 PC의 터미널에서 아래 명령어 호출하면 접속 가능함
$ curl localhost:3000      

Hello, world!                                                 

샘플 Manifest 사용해보기

|

Sample Manifest

snowdeer-app.yaml

apiVersion: v1
kind: Pod
metadata:
  name: snowdeer-app
  labels:
    app: snowdeer-app
spec:
  containers:
    - name: hello-server
      image: blux2/hello-server:1.0
      ports:
        - containerPort: 8080

실행

kubectl apply -f <파일이름>으로 실행할 수 있음

$ kubectl apply -f snowdeer-app.yaml

pod/snowdeer-app created

실행 확인

$ kubectl get pods    

NAME           READY   STATUS    RESTARTS   AGE
snowdeer-app   1/1     Running   0          81s

또는

$ kubectl get pods -o wide                

NAME           READY   STATUS    RESTARTS   AGE   IP           NODE                             NOMINATED NODE   READINESS GATES
snowdeer-app   1/1     Running   0          23m   10.244.0.5   snowdeer-cluster-control-plane   none             none

만약 -o wide 옵션 대신 -o yaml 옵션을 사용하면 다음과 같은 결과가 리턴

$ kubectl get pods snowdeer-app -o yaml    

apiVersion: v1
kind: Pod
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"v1","kind":"Pod","metadata":{"annotations":{},"labels":{"app":"snowdeer-app"},"name":"snowdeer-app","namespace":"default"},"spec":{"containers":[{"image":"blux2/hello-server:1.0","name":"hello-server","ports":[{"containerPort":8080}]}]}}
  creationTimestamp: "2025-12-07T07:06:14Z"
  generation: 1
  labels:
    app: snowdeer-app
  name: snowdeer-app
  namespace: default
  resourceVersion: "1578"
  uid: cd5a19de-fcf5-4daa-8373-c74295aea3c3
spec:
  containers:
  - image: blux2/hello-server:1.0
    imagePullPolicy: IfNotPresent
    name: hello-server
    ports:
    - containerPort: 8080
      protocol: TCP
    resources: {}
    terminationMessagePath: /dev/termination-log
    terminationMessagePolicy: File
    volumeMounts:
    - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
      name: kube-api-access-bh7dz
      readOnly: true
  dnsPolicy: ClusterFirst
  enableServiceLinks: true
  nodeName: snowdeer-cluster-control-plane
  preemptionPolicy: PreemptLowerPriority
  priority: 0
  restartPolicy: Always
  schedulerName: default-scheduler
  securityContext: {}
  serviceAccount: default
  serviceAccountName: default
  terminationGracePeriodSeconds: 30
  tolerations:
  - effect: NoExecute
    key: node.kubernetes.io/not-ready
    operator: Exists
    tolerationSeconds: 300
  - effect: NoExecute
    key: node.kubernetes.io/unreachable
    operator: Exists
    tolerationSeconds: 300
  volumes:
  - name: kube-api-access-bh7dz
    projected:
      defaultMode: 420
      sources:
      - serviceAccountToken:
          expirationSeconds: 3607
          path: token
      - configMap:
          items:
          - key: ca.crt
            path: ca.crt
          name: kube-root-ca.crt
      - downwardAPI:
          items:
          - fieldRef:
              apiVersion: v1
              fieldPath: metadata.namespace
            path: namespace
status:
  conditions:
  - lastProbeTime: null
    lastTransitionTime: "2025-12-07T07:06:23Z"
    observedGeneration: 1
    status: "True"
    type: PodReadyToStartContainers
  - lastProbeTime: null
    lastTransitionTime: "2025-12-07T07:06:14Z"
    observedGeneration: 1
    status: "True"
    type: Initialized
  - lastProbeTime: null
    lastTransitionTime: "2025-12-07T07:06:23Z"
    observedGeneration: 1
    status: "True"
    type: Ready
  - lastProbeTime: null
    lastTransitionTime: "2025-12-07T07:06:23Z"
    observedGeneration: 1
    status: "True"
    type: ContainersReady
  - lastProbeTime: null
    lastTransitionTime: "2025-12-07T07:06:14Z"
    observedGeneration: 1
    status: "True"
    type: PodScheduled
  containerStatuses:
  - containerID: containerd://f540bc09b42828c9e0a6783fe2e35bddd5a03e1ce07a1f024319097f7b721d8c
    image: docker.io/blux2/hello-server:1.0
    imageID: docker.io/blux2/hello-server@sha256:35ab584cbe96a15ad1fb6212824b3220935d6ac9d25b3703ba259973fac5697d
    lastState: {}
    name: hello-server
    ready: true
    resources: {}
    restartCount: 0
    started: true
    state:
      running:
        startedAt: "2025-12-07T07:06:23Z"
    user:
      linux:
        gid: 0
        supplementalGroups:
        - 0
        uid: 0
    volumeMounts:
    - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
      name: kube-api-access-bh7dz
      readOnly: true
      recursiveReadOnly: Disabled
  hostIP: 172.18.0.2
  hostIPs:
  - ip: 172.18.0.2
  observedGeneration: 1
  phase: Running
  podIP: 10.244.0.5
  podIPs:
  - ip: 10.244.0.5
  qosClass: BestEffort
  startTime: "2025-12-07T07:06:14Z"

kubectl get <리소스이름> -o yaml | less 옵션을 이용해서 key가 되는 문자열을 검색하기도 함

하지만, 다른 이유보다 아래와 같은 명령어를 이용하면 현재 클러스터에 적용된 오브젝트의 내용과 Manifest의 차이를 확인할 수 있는 유용성이 있음

$ kubectl get pods snowdeer-app -o yaml > pod.yaml

그 이후 아래 명령어로 차이를 확인

$ diff pod.yaml snowdeer-app.yaml

4,8c4
<   annotations:
<     kubectl.kubernetes.io/last-applied-configuration: |
<       {"apiVersion":"v1","kind":"Pod","metadata":{"annotations":{},"labels":{"app":"snowdeer-app"},"name":"snowdeer-app","namespace":"default"},"spec":{"containers":[{"image":"blux2/hello-server:1.0","name":"hello-server","ports":[{"containerPort":8080}]}]}}
<   creationTimestamp: "2025-12-07T07:06:14Z"
<   generation: 1
---
>   name: snowdeer-app
11,14d6
<   name: snowdeer-app
<   namespace: default
<   resourceVersion: "1578"
<   uid: cd5a19de-fcf5-4daa-8373-c74295aea3c3
17,129c9,12
<   - image: blux2/hello-server:1.0
<     imagePullPolicy: IfNotPresent
<     name: hello-server
<     ports:
<     - containerPort: 8080
<       protocol: TCP
<     resources: {}
<     terminationMessagePath: /dev/termination-log
<     terminationMessagePolicy: File
<     volumeMounts:
<     - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
<       name: kube-api-access-bh7dz
<       readOnly: true
<   dnsPolicy: ClusterFirst
<   enableServiceLinks: true
<   nodeName: snowdeer-cluster-control-plane
<   preemptionPolicy: PreemptLowerPriority
<   priority: 0
<   restartPolicy: Always
<   schedulerName: default-scheduler
<   securityContext: {}
<   serviceAccount: default
<   serviceAccountName: default
<   terminationGracePeriodSeconds: 30
<   tolerations:
<   - effect: NoExecute
<     key: node.kubernetes.io/not-ready
<     operator: Exists
<     tolerationSeconds: 300
<   - effect: NoExecute
<     key: node.kubernetes.io/unreachable
<     operator: Exists
<     tolerationSeconds: 300
<   volumes:
<   - name: kube-api-access-bh7dz
<     projected:
<       defaultMode: 420
<       sources:
<       - serviceAccountToken:
<           expirationSeconds: 3607
<           path: token
<       - configMap:
<           items:
<           - key: ca.crt
<             path: ca.crt
<           name: kube-root-ca.crt
<       - downwardAPI:
<           items:
<           - fieldRef:
<               apiVersion: v1
<               fieldPath: metadata.namespace
<             path: namespace
< status:
<   conditions:
<   - lastProbeTime: null
<     lastTransitionTime: "2025-12-07T07:06:23Z"
<     observedGeneration: 1
<     status: "True"
<     type: PodReadyToStartContainers
<   - lastProbeTime: null
<     lastTransitionTime: "2025-12-07T07:06:14Z"
<     observedGeneration: 1
<     status: "True"
<     type: Initialized
<   - lastProbeTime: null
<     lastTransitionTime: "2025-12-07T07:06:23Z"
<     observedGeneration: 1
<     status: "True"
<     type: Ready
<   - lastProbeTime: null
<     lastTransitionTime: "2025-12-07T07:06:23Z"
<     observedGeneration: 1
<     status: "True"
<     type: ContainersReady
<   - lastProbeTime: null
<     lastTransitionTime: "2025-12-07T07:06:14Z"
<     observedGeneration: 1
<     status: "True"
<     type: PodScheduled
<   containerStatuses:
<   - containerID: containerd://f540bc09b42828c9e0a6783fe2e35bddd5a03e1ce07a1f024319097f7b721d8c
<     image: docker.io/blux2/hello-server:1.0
<     imageID: docker.io/blux2/hello-server@sha256:35ab584cbe96a15ad1fb6212824b3220935d6ac9d25b3703ba259973fac5697d
<     lastState: {}
<     name: hello-server
<     ready: true
<     resources: {}
<     restartCount: 0
<     started: true
<     state:
<       running:
<         startedAt: "2025-12-07T07:06:23Z"
<     user:
<       linux:
<         gid: 0
<         supplementalGroups:
<         - 0
<         uid: 0
<     volumeMounts:
<     - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
<       name: kube-api-access-bh7dz
<       readOnly: true
<       recursiveReadOnly: Disabled
<   hostIP: 172.18.0.2
<   hostIPs:
<   - ip: 172.18.0.2
<   observedGeneration: 1
<   phase: Running
<   podIP: 10.244.0.5
<   podIPs:
<   - ip: 10.244.0.5
<   qosClass: BestEffort
<   startTime: "2025-12-07T07:06:14Z"
---
>     - name: hello-server
>       image: blux2/hello-server:1.0
>       ports:
>         - containerPort: 8080

실행해보면 차이가 많음. Manifest에서는 필수적인 내용만 기재되지만, 실제로 k8s에서 구동되면 더 많은 정보가 필요함

Kind 이용 클러스터 생성

|

k8s 클러스터 생성

아래와 같이 명령을 내리면 kind-가 prefix로 붙어서 kind-snowdeer-cluster 이름의 클러스터가 생성됨

$ kind create cluster --name snowdeer-cluster

Creating cluster "snowdeer-cluster" ...
 ✓ Ensuring node image (kindest/node:v1.34.0) 🖼 
 ✓ Preparing nodes 📦  
 ✓ Writing configuration 📜 
 ✓ Starting control-plane 🕹️ 
 ✓ Installing CNI 🔌 
 ✓ Installing StorageClass 💾 
Set kubectl context to "kind-snowdeer-cluster"
You can now use your cluster with:

kubectl cluster-info --context kind-snowdeer-cluster

Have a nice day! 👋

k8s 클러스터 정보 확인

$ kubectl cluster-info --context kind-snowdeer-cluster

Kubernetes control plane is running at https://127.0.0.1:57730
CoreDNS is running at https://127.0.0.1:57730/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.

위에서 --context 옵션을 붙여서 클러스터의 context를 지정해야 하지만, 만약 하나의 클러스터만 존재하면 해당 옵션은 생략가능함. 여러 클러스터일 경우 --context 옵션 없이 기본 클러스터를 지정하기 위해서 아래 명령어 사용 가능

$ kubectl config use-context kind-snowdeer-cluster

Switched to context "kind-snowdeer-cluster".

클러스터 리스트 확인

kind를 이용해서 클러스터를 생성했기 때문에 명령어를 kind를 이용해서 조회할 수 있음

$ kind get clusters   

snowdeer-cluster

Node 정보 확인

$ kubectl get nodes                               

NAME                             STATUS   ROLES           AGE     VERSION
snowdeer-cluster-control-plane   Ready    control-plane   6m41s   v1.34.0