무료로 쓰던 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으로 충분하다.
온라인 변경
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 한도 내에서는 과금되지 않는다
실측 검증 — 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이 아니라 단가를 잘못 계산한 결과였다.
관련 콘텐츠
Gateway API, Ingress를 대체하는 Kubernetes 표준
SIG-Network이 4년에 걸쳐 만든 Gateway API의 핵심 리소스 3가지와 그 관계를 이해하고, Ingress와 무엇이 달라졌는지 정리한다.
KubernetesKubernetes 노드 메모리 리밸런싱 — OOMKilled 해결과 워크로드 분산
Control Plane 노드에 메모리 대식가 워크로드가 몰려 OOMKilled가 반복됐다. fork()와 Copy-on-Write 메커니즘을 이해하고 affinity/toleration으로 워크로드를 분산한 과정.
KubernetesNGINX Ingress EOL 대응: OCI에서 Envoy Gateway로 마이그레이션
NGINX Ingress Controller 지원 종료에 대비해 OCI Always Free 클러스터에서 Envoy Gateway로 마이그레이션한 경험을 공유합니다.