Kubernetes OCI Kubernetes Envoy Gateway FinOps

무료로 쓰던 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 100Mbps16.4977%
Block Volume Storage 초과4.1719%
Block Volume Performance Units0.864%
합계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/v1alpha1
kind: EnvoyProxy
metadata:
name: oci-proxy-config
namespace: envoy-gateway-system
spec:
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/v1
kind: GatewayClass
metadata:
name: eg
spec:
controllerName: gateway.envoyproxy.io/gatewayclass-controller
parametersRef:
group: gateway.envoyproxy.io
kind: EnvoyProxy
namespace: envoy-gateway-system
name: oci-proxy-config

적용 후 OCI 콘솔에서 LB 상태를 확인하면:

OCI Load Balancer - Flexible 10Mbps 적용 결과

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-postgresqln8n 데이터베이스50GB
node-modules-n8n-0n8n 바이너리 데이터50GB
node-modules-n8n-worker-0n8n worker 모듈50GB
redis-data-n8n-redis-master-0n8n Redis 캐시50GB
data-garage-0Garage 데이터50GB
meta-garage-0Garage 메타데이터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로 재생성해야 한다.

Terminal window
kubectl delete statefulset n8n-worker n8n-redis-master -n n8n
helm upgrade n8n . -n n8n -f n8n.yaml
kubectl 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과금
0Lower Cost2무료
10Balanced60유료
20Higher Performance75유료

Always Free tier에서 과금을 피하려면 vpus=0이어야 한다. 내 Garage PV 2개가 vpus=10으로 설정되어 있었다.

Garage는 S3 호환 오브젝트 스토리지인데, Velero 백업 데이터를 저장하는 용도다. 높은 IOPS가 필요하지 않으므로 vpus=0으로 충분하다.

온라인 변경

vpus 변경은 Block Volume을 재생성할 필요 없이 온라인으로 가능하다. OCI 콘솔 또는 CLI에서 변경할 수 있다.

Terminal window
# 대상 확인
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 0

PROVISIONING 상태를 거쳐 1분 내에 AVAILABLE로 돌아온다. 서비스 중단 없이 변경 완료.


결과

항목변경 전변경 후
LB Shape100Mbps 고정 (SGD 16.49)Flexible 10Mbps (무료)
BV Storage394GB (SGD 4.17)294GB (SGD 0.43)
BV Performancevpus=10 x 2개 (SGD 0.86)vpus=0 (무료)
월 합계SGD 21.52SGD 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 100Mbps02/01~02/05SGD 3.01전환 전 5일 일할 과금. 이후 없음
BV Performance (vpus)02/05SGD 0.01변경 당일 잔여 과금. 이후 없음
BV Storage02/01~02/28SGD 3.20Boot 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이 아니라 단가를 잘못 계산한 결과였다.

관련 콘텐츠

댓글