무료로 쓰던 OCI에서 매달 21달러가 빠져나가고 있었다
OCI Always Free tier로 Kubernetes를 운영하다 예상치 못한 과금이 발생했다. Load Balancer Shape, Block Volume 용량, Performance Units — 세 가지 과금 원인을 찾고 월 SGD 21.52를 SGD 0.43까지 줄인 과정.
OCI(Oracle Cloud Infrastructure) Always Free tier는 개인 Kubernetes 클러스터를 운영하기에 꽤 좋은 조건을 제공한다. ARM 인스턴스 4 OCPU에 24GB RAM, Load Balancer 1개, Block Volume 200GB까지 무료다.
이 조건을 믿고 OKE(Oracle Kubernetes Engine) 클러스터에서 n8n, Garage 같은 서비스를 운영하고 있었는데, 어느 날 카드 명세서에 SGD 21.52(약 26,000원)가 찍혀 있었다. 매달 빠져나가고 있었다.
피같은 내돈. 무료로 만들어 보자
과금 원인 분석
OCI Cost Analysis에서 확인한 2026년 1월 청구 내역이다.
| 항목 | 월 비용 (SGD) | 비중 |
|---|---|---|
| Load Balancer 100Mbps | 16.49 | 77% |
| Block Volume Storage 초과 | 4.17 | 19% |
| Block Volume Performance Units | 0.86 | 4% |
| 합계 | 21.52 |
세 가지 항목 모두 Always Free 한도를 넘어서 발생한 과금이었다. 하나씩 살펴본다.
Load Balancer Shape — SGD 16.49/월 (77%)
가장 큰 과금 항목이었고, 원인을 찾기까지 가장 까다로웠다.
Always Free LB 조건
OCI Load Balancer Shape에는 두 가지 유형이 있다.
- Fixed — 100Mbps, 400Mbps 등 대역폭이 고정된 형태. 항상 유료
- Flexible — min/max 대역폭을 직접 지정하는 형태. 10Mbps~8000Mbps 범위
Always Free tier에서 무료로 쓰려면 두 가지 조건을 모두 만족해야 한다.
- Shape이 Flexible이어야 한다
- Bandwidth가 10Mbps (min/max 모두)여야 한다
내 클러스터의 LB를 확인해보니 100Mbps 고정 Shape이었다. Envoy Gateway를 설치할 때 별도 Shape 설정 없이 기본값으로 배포했는데, OCI CCM이 100Mbps 고정 LB를 생성한 것이다.
왜 WebUI에서 바꿀 수 없는가
OCI 콘솔에서 Load Balancer Shape을 직접 변경하면 되지 않을까? 안 된다.
Kubernetes 환경에서 Load Balancer는 OCI Cloud Controller Manager(CCM)가 관리한다. 흐름은 이렇다:
flowchart LR A[Service<br>type: LoadBalancer] -->|어노테이션 읽기| B[OCI CCM] B -->|API 호출| C[OCI Load Balancer]CCM은 Service 리소스의 어노테이션을 읽어서 OCI API로 LB를 생성하고 관리한다. 웹 콘솔에서 Shape을 바꿔도 CCM이 다음 reconcile에서 원래 상태로 되돌린다. Source of Truth는 Service 어노테이션이다.
그렇다면 Service 어노테이션을 직접 수정하면 될까? 이것도 안 된다. Envoy Gateway 환경에서 Service는 Envoy Gateway 컨트롤러가 자동 생성한다. 수동으로 어노테이션을 수정해도 컨트롤러가 다시 덮어쓴다.
설정 경로: EnvoyProxy → GatewayClass → Service → OCI CCM
Envoy Gateway에서 LB 설정을 제어하는 올바른 경로는 EnvoyProxy 리소스를 사용하는 것이다.
flowchart LR A[EnvoyProxy] -->|parametersRef| B[GatewayClass] B -->|Service 생성| C[Envoy Gateway<br>Controller] C -->|어노테이션 포함| D[Service<br>type: LoadBalancer] D -->|어노테이션 읽기| E[OCI CCM] E -->|API 호출| F[OCI LB<br>Flexible 10Mbps]EnvoyProxy 리소스에 OCI LB 어노테이션을 정의하고, GatewayClass가 이를 참조하면, Envoy Gateway 컨트롤러가 Service를 생성할 때 해당 어노테이션을 자동으로 포함시킨다.
# EnvoyProxy — OCI LB Shape 어노테이션 정의apiVersion: gateway.envoyproxy.io/v1alpha1kind: EnvoyProxymetadata: name: oci-proxy-config namespace: envoy-gateway-systemspec: provider: type: Kubernetes kubernetes: envoyService: annotations: service.beta.kubernetes.io/oci-load-balancer-shape: "flexible" service.beta.kubernetes.io/oci-load-balancer-shape-flex-min: "10" service.beta.kubernetes.io/oci-load-balancer-shape-flex-max: "10"# GatewayClass — EnvoyProxy 참조apiVersion: gateway.networking.k8s.io/v1kind: GatewayClassmetadata: name: egspec: controllerName: gateway.envoyproxy.io/gatewayclass-controller parametersRef: group: gateway.envoyproxy.io kind: EnvoyProxy namespace: envoy-gateway-system name: oci-proxy-config적용 후 OCI 콘솔에서 LB 상태를 확인하면:

Shape이 Flexible로 바뀌고 Bandwidth가 10Mbps로 설정되었다. 기존 공인 IP가 그대로 유지되면서 Shape만 변경되었다. DNS나 서비스에 영향 없이 과금만 사라진다.
놓치기 쉬운 이유
이 설정을 놓치는 사람이 많을 것 같다. 이유는:
- Helm chart 기본값에 LB Shape 설정이 없다 — Envoy Gateway를
helm install로 설치하면 GatewayClass에parametersRef가 없고, EnvoyProxy 리소스도 생성되지 않는다 - OCI 콘솔에서 직접 바꿀 수 없다 — Kubernetes가 관리하는 리소스를 콘솔에서 수정해도 되돌아간다
- 과금 시작 시점이 명확하지 않다 — Always Free 계정이라 처음 몇 달은 크레딧으로 상쇄되어 청구서에 안 잡힐 수 있다
다른 Gateway API 구현체(Istio 등)를 사용하더라도 원리는 같다. 해당 구현체가 Service를 생성할 때 OCI LB 어노테이션을 주입하는 방법을 찾아야 한다.
Block Volume Storage — SGD 4.17/월 (19%)
Always Free BV 조건
OCI Always Free tier의 Block Volume 한도는 200GB다 (Boot Volume 포함).
내 클러스터에서 사용 중인 PV를 확인하니:
| PV | 용도 | 크기 |
|---|---|---|
| n8n-postgresql | n8n 데이터베이스 | 50GB |
| node-modules-n8n-0 | n8n 바이너리 데이터 | 50GB |
| node-modules-n8n-worker-0 | n8n worker 모듈 | 50GB |
| redis-data-n8n-redis-master-0 | n8n Redis 캐시 | 50GB |
| data-garage-0 | Garage 데이터 | 50GB |
| meta-garage-0 | Garage 메타데이터 | 50GB |
Boot Volume(노드 2개 x 47GB = 94GB)까지 합치면 394GB. 200GB 한도를 거의 두 배 초과하고 있었다.
불필요한 PV 식별
6개 PV 중 영속 스토리지가 정말 필요한 것은 어디일까?
- n8n-postgresql: 데이터베이스 — 영속 필수
- node-modules-n8n-0: n8n 바이너리 데이터 — 영속 필수
- node-modules-n8n-worker-0: n8n worker 노드 모듈 — Pod 재시작 시 다시 설치 가능
- redis-data-n8n-redis-master-0: Redis 캐시 — 캐시는 휘발되어도 된다
- data-garage-0, meta-garage-0: Garage S3 스토리지 — 영속 필수
Worker의 node_modules와 Redis 캐시는 Pod 재시작 시 자동 복구되므로 emptyDir로 충분하다.
Helm values 변경
# n8n Helm values — Worker와 Redis를 emptyDir로 전환worker: persistence: enabled: false forceToUseStatefulset: false # Deployment로 전환 volumes: - name: node-modules emptyDir: {}redis: master: persistence: enabled: false한 가지 주의할 점이 있다. n8n chart에서 worker.persistence.enabled: false로 설정하면 volumeClaimTemplates는 제거되지만, initContainer에서 참조하는 node-modules volume까지 제거되지는 않는다. worker.volumes에 emptyDir을 명시적으로 선언해야 Pod가 정상 기동한다.
StatefulSet의 volumeClaimTemplates는 변경이 불가능하므로 기존 StatefulSet을 삭제하고 Helm upgrade로 재생성해야 한다.
kubectl delete statefulset n8n-worker n8n-redis-master -n n8nhelm upgrade n8n . -n n8n -f n8n.yamlkubectl delete pvc node-modules-n8n-worker-0 redis-data-n8n-redis-master-0 -n n8n결과: PV 6개 → 4개, Block Volume 300GB → 200GB.
하지만 이것만으로는 Free 한도를 맞출 수 없다. 200GB 한도에는 Boot Volume도 포함되기 때문이다.
Boot Volume — 줄일 수 없는 94GB
OKE 노드 2대의 Boot Volume이 각각 47GB, 합산 94GB를 차지하고 있다.
| 볼륨 유형 | 용량 |
|---|---|
| Block Volume (PV 4개) | 200GB |
| Boot Volume (노드 2개) | 94GB |
| 합계 | 294GB |
200GB 한도를 94GB 초과한다. 그런데 이걸 줄이기가 어렵다.
- Boot Volume: OKE 노드의 OS 디스크다. 47GB가 최소 크기이고, 노드를 없애지 않는 한 줄일 수 없다
- Block Volume: OCI Block Volume의 최소 크기가 50GB다. PV 4개가 모두 이미 최소값이라 더 줄일 수 없다
Boot Volume을 감안하면 Block Volume은 200 - 94 = 106GB 이내여야 한도에 맞는데, 50GB PV 2개(100GB)만 쓰더라도 나머지 2개를 포기해야 한다. Garage나 PostgreSQL처럼 영속이 필수인 서비스가 4개이므로 현실적으로 불가능하다.
이 초과분은 약 SGD 0.43/월의 과금으로 이어진다. 완전한 무료는 아니지만, 원래 SGD 4.17에서 90% 이상 줄인 셈이다.
Block Volume Performance Units — SGD 0.86/월 (4%)
vpus란
OCI Block Volume에는 vpus(Volume Performance Units per GB) 라는 성능 등급 설정이 있다.
| vpus | 등급 | IOPS/GB | 과금 |
|---|---|---|---|
| 0 | Lower Cost | 2 | 무료 |
| 10 | Balanced | 60 | 유료 |
| 20 | Higher Performance | 75 | 유료 |
Always Free tier에서 과금을 피하려면 vpus=0이어야 한다. 내 Garage PV 2개가 vpus=10으로 설정되어 있었다.
Garage는 S3 호환 오브젝트 스토리지인데, Velero 백업 데이터를 저장하는 용도다. 높은 IOPS가 필요하지 않으므로 vpus=0으로 충분하다.
[정정 2026-07-28] 이 판단에는 조건이 빠져 있었다. 여기서 본 것은 정상 운영 중의 IOPS뿐이고, 파드가 재생성될 때 드는 작업량을 보지 않았다. 이 문단대로 vpus=0으로 내린
data-garage-0볼륨은 173일 뒤 노드 교체에서 컨테이너 생성이 kubelet의 2분 타임아웃을 넘겨 파드를 영구히 차단했다. 임계점은 vpus 값 자체가 아니라 vpus × 볼륨 크기 × 저장된 파일 개수다. 백업 저장소는 평소 IOPS는 낮게 쓰면서 객체 수는 계속 늘어나므로 하필 이 함정에 가장 잘 걸린다. 자세한 내용은 OCI를 Always Free로 운영할 때 생기는 이슈 — Block Volume VPU 0에 정리했다.
온라인 변경
vpus 변경은 Block Volume을 재생성할 필요 없이 온라인으로 가능하다. OCI 콘솔 또는 CLI에서 변경할 수 있다.
# 대상 확인oci bv volume list --compartment-id <compartment-id> --all \ --query 'data[*].{"name":"display-name","vpus":"vpus-per-gb","state":"lifecycle-state"}' \ --output table
# vpus 변경 (다운타임 없음)oci bv volume update --volume-id <OCID> --vpus-per-gb 0PROVISIONING 상태를 거쳐 1분 내에 AVAILABLE로 돌아온다. 서비스 중단 없이 변경 완료.
결과
| 항목 | 변경 전 | 변경 후 |
|---|---|---|
| LB Shape | 100Mbps 고정 (SGD 16.49) | Flexible 10Mbps (무료) |
| BV Storage | 394GB (SGD 4.17) | 294GB (SGD 0.43) |
| BV Performance | vpus=10 x 2개 (SGD 0.86) | vpus=0 (무료) |
| 월 합계 | SGD 21.52 | SGD 0.43 |
SGD 21.52에서 SGD 0.43으로, 98% 절감했다. 연간으로 치면 SGD 253, 한화로 약 30만원 절감이다.
남은 SGD 0.43은 Boot Volume 때문이다. OKE 노드 2대의 Boot Volume(94GB)이 200GB 한도에 포함되는데, Boot Volume은 노드의 OS 디스크라 줄이거나 없앨 수 없다. Block Volume 최소 크기도 50GB라서 PV를 더 줄이는 것도 불가능하다. 아메리카노 한 잔 정도야 뭐
핵심 정리
OCI Always Free tier에서 Kubernetes를 운영할 때 과금이 발생한다면 체크 순서:
- Load Balancer Shape — Flexible 10Mbps여야 무료. Kubernetes 환경에서는 WebUI가 아니라 Service 어노테이션이 Source of Truth이며, Gateway API 구현체를 쓴다면 해당 구현체의 설정 경로(EnvoyProxy 등)를 통해 어노테이션을 주입해야 한다
- Block Volume 용량 — Boot + Block 합산 200GB 이내. 캐시나 임시 데이터는 emptyDir로 전환. 단, Boot Volume(노드당 47GB)도 한도에 포함되므로 노드 수에 따라 PV로 쓸 수 있는 용량이 제한된다
- Block Volume Performance — Block Volume은 vpus=0 (Lower Cost)으로 설정해야 무료. OCI CLI로 온라인 변경 가능, 다운타임 없음. Boot Volume은 vpus=10이 최소값이지만 Always Free 한도 내에서는 과금되지 않는다. 단 파일 개수가 많은 볼륨은 vpus=0으로 내리면 다음 파드 재생성 때 기동이 실패할 수 있다 (정정 내용)
실측 검증 — 2026년 2월 청구서
최적화 완료(2월 5일) 후 첫 청구서로 실제 과금을 확인했다.
| 항목 | 기간 | 금액 | 설명 |
|---|---|---|---|
| LB 100Mbps | 02/01~02/05 | SGD 3.01 | 전환 전 5일 일할 과금. 이후 없음 |
| BV Performance (vpus) | 02/05 | SGD 0.01 | 변경 당일 잔여 과금. 이후 없음 |
| BV Storage | 02/01~02/28 | SGD 3.20 | Boot Volume 94GB 지속 과금 |
| 합계 | SGD 6.22 |
LB와 vpus 항목은 전환 기간 일할 과금으로, 3월부터는 청구되지 않는다.
BV Storage는 예상치(SGD 0.43)와 차이가 있었다. 실제 단가를 적용하면:
- Block Volume 4개(200GB) = 무료 한도 정확히 소진 → 과금 없음
- Boot Volume 2대(94GB) = 초과분으로 과금
- OCI 단가 $0.0255 USD/GB/월 ≈ SGD 0.034/GB/월
- 94GB × SGD 0.034 = SGD 3.20/월
3월 이후 지속 과금은 SGD 3.20/월이다. 원래 추정한 SGD 0.43이 아니라 단가를 잘못 계산한 결과였다.
관련 콘텐츠
OCI를 Always Free로 운영할 때 생기는 이슈 — Block Volume VPU 0
Always Free 한도를 맞추려고 Block Volume 성능 등급을 0으로 내렸습니다. 173일 동안 아무 일도 없다가, 클러스터 업그레이드로 파드가 재생성되는 순간 컨테이너 생성이 kubelet 타임아웃을 넘겼습니다. 원인에 도달하기까지 세운 가설 5개와 전부 틀린 이유를 정리했습니다.
KubernetesGateway API 전환기 (1) - Cilium을 Kubespray에서 Helm으로
Kubespray로 설치한 Cilium을 Helm 관리로 전환하는 과정에서 겪은 트러블슈팅과 교훈을 공유합니다.
KubernetesNGINX Ingress EOL 대응: OCI에서 Envoy Gateway로 마이그레이션
NGINX Ingress Controller 지원 종료에 대비해 OCI Always Free 클러스터에서 Envoy Gateway로 마이그레이션한 경험을 공유합니다.