OpenFeature + Flagd로 Feature Flag 표준화하기 — 벤더 종속 없이 Kubernetes에서 점진적 롤아웃과 A/B 테스트를 코드로 관리하는 구조
LaunchDarkly나 Split 같은 유료 플래그 서비스를 쓰다 보면 어느 순간 불편한 현실을 직면하게 됩니다. 플래그 로직이 벤더 SDK에 깊숙이 박혀 있어서, 서비스를 교체하려면 코드 전체를 다시 뜯어고쳐야 합니다. 마이크로서비스가 늘어나고 팀이 커질수록 이 문제는 조용히 기술 부채로 쌓입니다.
OpenFeature는 CNCF 인큐베이팅 프로젝트로, 이 문제를 정면으로 다루는 벤더 중립 피처 플래그 표준입니다. Flagd는 그 표준에 맞게 설계된 오픈소스 플래그 평가 데몬으로, Kubernetes에서 사이드카로 실행하거나 단독 프로세스로 올릴 수 있습니다. 두 조합으로 플래그 정의를 Git에 YAML/JSON으로 선언하고, ArgoCD나 Flux로 자동 동기화하면 플래그 변경이 PR → 리뷰 → Merge → 자동 반영 흐름 안으로 들어옵니다. 초기 구성을 마친 이후에는, 배포 없이 파드 재시작 없이.
이 글은 Kubernetes 환경에서 OpenFeature + Flagd를 실제로 연결하는 구조를 다룹니다. 점진적 롤아웃·A/B 테스트·킬 스위치 시나리오를 코드와 함께 살펴보고, 사이드카 자동 주입부터 GitOps 통합까지 벤더 종속 없이 플래그 인프라를 구성하는 방법을 설명합니다.
핵심 개념
Provider 패턴: 플래그 백엔드를 교체 가능하게 만드는 구조
OpenFeature의 핵심은 Provider 패턴입니다. 애플리케이션은 항상 OpenFeature SDK의 표준 인터페이스만 호출하고, 실제 플래그를 평가하는 백엔드는 Provider 구현체로 분리됩니다. Flagd를 쓰다가 LaunchDarkly로 바꾸거나, 반대로 돌아오거나, 둘을 동시에 쓰거나 — 비즈니스 코드는 건드리지 않아도 됩니다.
Go로 작성된 예시입니다.
import (
"context"
"log"
"github.com/open-feature/go-sdk/openfeature"
flagd "github.com/open-feature/go-sdk-contrib/providers/flagd/pkg"
)
func main() {
openfeature.SetProvider(flagd.NewProvider())
client := openfeature.NewClient("my-service")
enabled, err := client.BooleanValue(
context.Background(),
"new-checkout-flow",
false, // 플래그 평가 실패 시 폴백 기본값
openfeature.EvaluationContext{},
)
if err != nil {
log.Printf("flag evaluation error: %v", err)
}
if enabled {
// 신기능 실행
}
}flagd.NewProvider()를 다른 구현체로 교체해도 아래의 비즈니스 코드는 변경 없이 그대로 동작합니다. Python이든 Java든 .NET이든 동일한 개념 모델(Provider, Hook, EvaluationContext)을 공유하기 때문에, 폴리글랏 환경에서도 팀 전체가 같은 패턴으로 플래그를 다룰 수 있습니다.
Flagd: 플래그 평가에 집중하는 데몬
Flagd는 "한 가지 일을 잘 하는" 데몬입니다. 플래그를 정의 파일에서 읽어서 평가 결과를 반환합니다. 상태를 갖지 않는(stateless) 경량 바이너리로, 플래그 소스로 파일·HTTP 엔드포인트·Kubernetes CRD·gRPC 서비스를 지원합니다.
플래그 정의는 JSON으로 이렇게 생겼습니다.
{
"flags": {
"new-checkout-flow": {
"state": "ENABLED",
"variants": { "on": true, "off": false },
"defaultVariant": "off",
"targeting": {
"fractional": [["on", 20], ["off", 80]]
}
},
"checkout-button-color": {
"state": "ENABLED",
"variants": { "blue": "blue", "green": "green" },
"defaultVariant": "blue"
}
}
}앱은 localhost:8013(gRPC) 또는 localhost:8016(HTTP)으로 플래그를 평가합니다. Flagd는 OFREP(OpenFeature Remote Evaluation Protocol)이 표준화되기 이전부터 gRPC 기반 원격 평가를 제공해 왔습니다. OFREP의 기여는 이 원격 평가를 네트워크 수준에서 표준화한 데 있습니다. POST /ofrep/v1/evaluate/flags/{key} 형태의 REST API 명세 덕분에, Flagd가 아닌 다른 OFREP 호환 백엔드로 교체할 때도 SDK 변경 없이 네트워크 수준의 호환성이 보장됩니다.
OpenFeature Operator: 어노테이션으로 사이드카 자동 주입
Kubernetes 환경에서의 핵심 편의 기능입니다. OpenFeature Operator를 클러스터에 설치하면, Deployment 어노테이션 두 줄로 Flagd 사이드카가 자동으로 주입됩니다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-service
spec:
template:
metadata:
annotations:
openfeature.dev/enabled: "true"
openfeature.dev/featureflagsource: "my-flag-source"Operator는 Helm 차트로 설치할 수 있습니다.
helm repo add open-feature-operator https://open-feature.github.io/open-feature-operator/
helm install openfeature open-feature-operator/open-feature-operator \
--namespace open-feature-operator-system \
--create-namespace플래그 소스를 CRD로 선언합니다.
apiVersion: core.openfeature.dev/v1beta1
kind: FeatureFlagSource
metadata:
name: my-flag-source
spec:
sources:
- source: my-namespace/my-feature-flags
provider: kubernetesapiVersion: core.openfeature.dev/v1beta1
kind: FeatureFlag
metadata:
name: my-feature-flags
namespace: my-namespace
spec:
flagSpec:
flags:
new-checkout-flow:
state: ENABLED
variants:
"on": true
"off": false
defaultVariant: "off"
targeting:
fractional:
- ["on", 20]
- ["off", 80]실전 적용
점진적 롤아웃 — fractional로 5%부터 시작하기
신기능 배포에서 가장 큰 위험은 전체 트래픽에 한 번에 올리는 것입니다. Flagd의 fractional 타겟팅을 쓰면 이걸 플래그 정의 수정 하나로 제어할 수 있습니다.
5% → 20% → 50% → 100% 단계별로 플래그 정의만 바꾸면 됩니다.
{
"flags": {
"new-checkout-flow": {
"state": "ENABLED",
"variants": { "on": true, "off": false },
"defaultVariant": "off",
"targeting": {
"fractional": [
["on", 5],
["off", 95]
]
}
}
}
}여기서 한 가지 알아두어야 할 동작이 있습니다. fractional 룰의 비율 합계가 100%인 경우, 모든 요청이 해당 룰로 버킷팅됩니다. 이 상태에서는 defaultVariant가 참조되지 않습니다. 즉 [["on", 5], ["off", 95]]처럼 합이 100%인 구성에서 defaultVariant를 바꿔도 동작이 달라지지 않습니다.
롤백 방법은 두 가지입니다.
첫째, state를 DISABLED로 변경합니다. 타겟팅 룰 자체가 평가되지 않고, 애플리케이션 코드에서 BooleanValue()에 전달한 기본값(false)이 반환됩니다.
{
"flags": {
"new-checkout-flow": {
"state": "DISABLED",
"variants": { "on": true, "off": false },
"defaultVariant": "off"
}
}
}둘째, fractional을 명시적으로 [["off", 100]]으로 수정합니다.
{
"flags": {
"new-checkout-flow": {
"state": "ENABLED",
"variants": { "on": true, "off": false },
"defaultVariant": "off",
"targeting": {
"fractional": [["off", 100]]
}
}
}
}파드 재배포는 필요 없습니다. 단, ArgoCD를 통한 반영은 기본 폴링 주기(3분) 이내에 이루어집니다. 더 빠른 반영이 필요하다면 ArgoCD 웹훅을 설정하면 됩니다.
애플리케이션 코드는 이렇게 씁니다.
enabled, err := client.BooleanValue(
ctx,
"new-checkout-flow",
false, // 기본값: DISABLED 상태이거나 평가 실패 시 반환
openfeature.EvaluationContext{
TargetingKey: userID, // 동일 사용자는 항상 같은 결과
},
)
if err != nil {
log.Printf("flag evaluation error: %v", err)
}TargetingKey로 사용자 ID를 넘기면 동일 사용자는 항상 같은 배리언트를 받아 UX 일관성이 유지됩니다.
A/B 테스트 — 배리언트로 지표를 측정하기
점진적 롤아웃과 A/B 테스트는 둘 다 fractional 타겟팅을 사용하지만 목적이 다릅니다.
점진적 롤아웃은 리스크 분산이 목적입니다. 배포 초기에 트래픽 일부에 신기능을 노출하고 에러율·지연 시간을 모니터링한 뒤, 문제가 없으면 비율을 높입니다. 지표가 안정적이면 100%로 올리고 플래그를 제거하는 것이 목표입니다.
A/B 테스트는 지표 측정이 목적입니다. 전환율이나 클릭률처럼 특정 비즈니스 지표가 어느 배리언트에서 높게 나오는지 통계적으로 비교합니다. 따라서 A/B 테스트는 충분한 표본이 모일 때까지 비율을 바꾸지 않는 것이 원칙입니다. 중간에 비율을 바꾸면 실험 결과가 오염됩니다.
버튼 색상을 A/B 테스트하는 시나리오입니다. FeatureFlag CRD에 배리언트와 비율을 선언하고, 애플리케이션에서 평가 결과를 받아 분기합니다.
apiVersion: core.openfeature.dev/v1beta1
kind: FeatureFlag
metadata:
name: checkout-ab-test
namespace: my-namespace
spec:
flagSpec:
flags:
checkout-button-color:
state: ENABLED
variants:
blue: "blue"
green: "green"
defaultVariant: "blue"
targeting:
fractional:
- ["blue", 50]
- ["green", 50]// TypeScript 예시
const buttonColor = await client.getStringValue(
"checkout-button-color",
"blue", // 기본값
{ targetingKey: userId }
);
renderButton({ color: buttonColor });실험 결과를 OpenTelemetry 트레이스에 연결하려면 OpenTelemetryHook을 명시적으로 등록해야 합니다. Hook을 등록하면 플래그 평가 결과가 spans에 자동으로 첨부되어 배리언트별 성능 지표를 비교할 수 있습니다. 연동 방법은 참고 자료 섹션의 OpenTelemetry 가이드를 참조하세요.
GitOps 통합 — ArgoCD로 플래그 변경 자동 동기화
플래그 변경을 Git PR로 관리하면 변경 이력·코드 리뷰·자동 롤백이 모두 따라옵니다. 누가 언제 어떤 이유로 트래픽 비율을 바꿨는지 git log 한 줄로 파악할 수 있고, 문제가 생기면 revert PR 하나로 되돌릴 수 있습니다. 플래그 관리 UI에서 클릭 한 번으로 값을 바꾸는 방식과 결정적으로 다른 점이 여기에 있습니다.
ArgoCD Application 설정에서 FeatureFlag CRD가 위치한 디렉터리를 path로 지정합니다.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: feature-flags
spec:
source:
repoURL: https://github.com/your-org/your-repo
targetRevision: main
path: k8s/feature-flags
destination:
server: https://kubernetes.default.svc
namespace: my-namespace
syncPolicy:
automated:
prune: true
selfHeal: truek8s/feature-flags/ 디렉터리에 있는 FeatureFlag YAML을 수정하고 PR을 올리면, Merge 후 ArgoCD가 클러스터에 반영합니다. CRD가 업데이트되면 Flagd는 파드 재시작 없이 인메모리 플래그를 갱신합니다. Merge에서 실제 반영까지는 ArgoCD 폴링 주기(기본 3분) 이내가 소요됩니다. 더 빠른 반영이 필요하다면 GitHub 웹훅과 ArgoCD를 연결하면 Merge 직후 동기화됩니다.
킬 스위치 — 외부 서비스 장애 시 기능 격리
Flagd의 중요한 특성 중 하나는 소스가 오프라인이어도 인메모리 캐시에서 마지막으로 알려진 값을 반환한다는 점입니다. 카스케이딩 장애를 막는 데 유용합니다.
킬 스위치 패턴은 fractional 타겟팅 룰을 두지 않는 것이 핵심입니다. fractional 룰이 없으면 모든 요청에 defaultVariant가 직접 적용됩니다. 앞서 살펴본 점진적 롤아웃 플래그와 구조가 다른 이유가 여기에 있습니다. 롤아웃 플래그에서는 fractional 룰이 트래픽 100%를 버킷팅해 defaultVariant가 참조되지 않았지만, 킬 스위치 플래그는 타겟팅 룰이 없으므로 defaultVariant 값만 바꿔도 즉시 전체 트래픽에 영향을 줍니다.
# 평소 상태: 결제 게이트웨이 활성화
apiVersion: core.openfeature.dev/v1beta1
kind: FeatureFlag
metadata:
name: payment-gateway-flag
namespace: my-namespace
spec:
flagSpec:
flags:
external-payment-gateway:
state: ENABLED
variants:
"on": true
"off": false
defaultVariant: "on"# 장애 발생 시: defaultVariant만 변경 후 Merge
apiVersion: core.openfeature.dev/v1beta1
kind: FeatureFlag
metadata:
name: payment-gateway-flag
namespace: my-namespace
spec:
flagSpec:
flags:
external-payment-gateway:
state: ENABLED
variants:
"on": true
"off": false
defaultVariant: "off"PR 하나, Merge 하나로 전체 트래픽의 해당 기능이 비활성화됩니다. 애플리케이션은 false 기본값 분기로 폴백 동작을 수행하고, 외부 API를 더 이상 호출하지 않습니다.
단, ArgoCD 폴링 주기가 있으므로 Merge 후 최대 3분의 지연이 있을 수 있습니다. 장애 대응처럼 즉각적인 격리가 필요한 상황이라면 kubectl apply로 CRD를 직접 적용하는 것이 빠릅니다. GitOps 흐름을 유지하고 싶다면 ArgoCD 웹훅을 미리 설정해두는 것을 권장합니다.
장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 벤더 독립성 | SDK 인터페이스가 고정되어 있어 LaunchDarkly → Flagd → ConfigCat 교체 시 애플리케이션 코드를 수정하지 않아도 됩니다 |
| 다국어 표준화 | Go·Java·JS·TS·Python·.NET·PHP·Ruby SDK가 동일한 개념 모델을 공유해 폴리글랏 환경에서 일관성을 확보합니다 |
| 테스트 용이성 | SDK 내장 InMemoryProvider로 네트워크 없이 단위 테스트에서 정확한 플래그 상태를 주입할 수 있습니다 |
| 관측 가능성 | OpenTelemetryHook 등록 시 플래그 평가 결과가 분산 트레이스에 자동 첨부됩니다 |
| GitOps 친화성 | 플래그 정의가 YAML/JSON으로 Git에 저장되어 변경 이력·PR 리뷰·자동 배포가 가능합니다 |
| CNCF 생태계 통합 | Kubernetes Operator·Helm 차트·ArgoCD·Flux 등 CNCF 스택과 자연스럽게 통합됩니다 |
| 장애 복원력 | 소스 오프라인 시 인메모리 캐시에서 마지막 알려진 값을 반환해 카스케이딩 장애를 방지합니다 |
InMemoryProvider는 단위 테스트에서 특히 유용합니다. Flagd 없이도 플래그 동작을 정확히 제어할 수 있습니다.
func TestNewCheckoutFlowEnabled(t *testing.T) {
provider := openfeature.NewInMemoryProvider(map[string]openfeature.InMemoryFlag{
"new-checkout-flow": {
DefaultVariant: "on",
Variants: map[string]interface{}{
"on": true,
"off": false,
},
},
})
openfeature.SetProvider(provider)
client := openfeature.NewClient("test")
enabled, err := client.BooleanValue(
context.Background(), "new-checkout-flow", false,
openfeature.EvaluationContext{TargetingKey: "user-1"},
)
if err != nil {
t.Fatal(err)
}
if !enabled {
t.Error("expected new-checkout-flow to be enabled")
}
}단점 및 주의사항
| 항목 | 내용 |
|---|---|
| UI 대시보드 부재 | Flagd 자체는 관리 UI를 제공하지 않습니다. 시각적 관리가 필요하면 Flipt나 Unleash 같은 외부 백엔드로 보완이 필요합니다 |
| 소규모 프로젝트 오버헤드 | 플래그 5개 이하의 단일 서비스에서는 추상화 계층이 불필요한 복잡도를 더할 수 있습니다 |
| Provider 성숙도 편차 | 각 벤더 Provider의 완성도가 다릅니다. 스트리밍 업데이트나 OFREP를 완전히 지원하지 않는 Provider가 존재합니다 |
| 마이그레이션 비자동화 | 기존 플래그 정의·타겟팅 룰·환경 설정은 수동으로 재구성해야 합니다. SDK 교체만으로 모든 게 해결되지 않습니다 |
| CRD 의존성 | Kubernetes Operator 방식은 CRD와 오퍼레이터를 클러스터에 별도 설치·관리해야 하며, Operator 업그레이드 시 CRD 스키마 변경에 주의가 필요합니다 |
| 사이드카 리소스 오버헤드 | Flagd 사이드카가 모든 Pod에 주입되면 파드당 추가 CPU/메모리가 발생합니다. 대규모 클러스터에서는 Flagd-proxy 공유 모드 사용을 검토할 수 있습니다. Flagd-proxy는 여러 파드가 단일 Flagd 인스턴스를 공유하는 구성으로, 사이드카를 개별 주입하는 대신 클러스터 내 하나의 Flagd 데몬에 요청을 집중시킵니다 |
실무에서 흔한 실수
TargetingKey 빠뜨리기: fractional 타겟팅은 TargetingKey 없이 호출하면 매 호출마다 랜덤 배리언트를 반환합니다. 동일 사용자가 같은 배리언트를 받으려면 반드시 사용자 ID를 TargetingKey로 넘겨야 합니다. A/B 테스트 결과가 이상하다면 여기부터 확인해보세요.
사이드카 없는 환경에서 연결 오류: 로컬 개발 환경에는 Operator가 없으므로 어노테이션을 붙여도 사이드카가 주입되지 않습니다. 로컬에서는 InMemoryProvider를 사용하거나 Flagd 바이너리를 별도 프로세스로 실행하는 것이 편합니다.
CRD 버전 관리 소홀: Operator 버전을 올릴 때 CRD 스키마가 바뀌는 경우가 있습니다. Helm 업그레이드 전에 반드시 릴리스 노트에서 CRD 변경 사항을 확인하는 습관이 필요합니다.
마치며
OpenFeature + Flagd 조합의 핵심 가치는 플래그를 코드처럼 관리한다는 데 있습니다. 플래그 정의가 Git에 있으면 변경 이력이 남고, PR로 리뷰되고, ArgoCD로 자동 반영됩니다. 벤더를 바꾸고 싶을 때 Provider 구현체만 교체하면 되고, 파드 재배포 없이 트래픽 비율을 조절할 수 있고, 장애 시 플래그 하나로 기능을 격리할 수 있습니다.
물론 모든 팀에 맞는 것은 아닙니다. 플래그 수가 적고 팀 규모가 작다면 직접 구현이 더 단순할 수 있습니다. 하지만 마이크로서비스가 여러 개이고, 폴리글랏 환경이고, 점진적 롤아웃이 실제로 필요한 상황이라면 OpenFeature는 진지하게 검토할 만한 선택지입니다.
시작해볼 수 있는 3단계:
-
OpenFeature SDK +
InMemoryProvider로 단위 테스트부터 — 기존 코드에 OpenFeature SDK를 붙이고, 프로덕션 Provider 없이InMemoryProvider로 플래그 동작을 테스트해볼 수 있습니다. 네트워크도, Kubernetes도 필요 없습니다. -
Flagd를 로컬에서 단독 실행해보기 — JSON 플래그 파일 하나 만들고 Flagd 바이너리를 로컬에서 실행하면,
localhost:8016으로 플래그 평가가 작동합니다. 사이드카 없이도 전체 흐름을 확인할 수 있습니다. -
Operator + ArgoCD 연동으로 GitOps 파이프라인 구성 — 개발 클러스터에 OpenFeature Operator를 설치하고,
FeatureFlagCRD를 Git에 올린 뒤 ArgoCD로 자동 동기화를 설정하면 프로덕션과 동일한 흐름을 경험할 수 있습니다.
참고 자료
- OpenFeature 공식 문서
- Flagd 공식 사이트
- OpenFeature Operator GitHub
- CNCF OpenFeature 프로젝트 페이지
- OFREP 프로토콜 명세
- OpenFeature Quick Start — Kubernetes Operator
- GitOps로 피처 플래그 관리하기 — ArgoCD
- OpenTelemetry로 A/B 테스트 배리언트 연관짓기
- OpenFeature Operator ArtifactHub Helm 차트
- Is it Observable? — OpenFeature + Flagd 심층 분석
- SigNoz의 OpenFeature 가이드
- OpenFeature — 피처 플래그를 1등 시민으로