Linkerd Service Mesh
GenD 의 service-to-service 통신은 Linkerd 2.14 stable line 으로 자동 mTLS 보호된다 (Phase 13.1-4 — #519, #770-#799).
적용 현황 (prod, 2026-05-13 기준)
- 22 워크로드 mesh 가입 — gend-ui, dagster-webserver, dagster-daemon, dagster-oauth2-proxy, mlflow, kafka-ui, kafka-connect, JupyterHub hub/proxy, gend-api, seaweedfs, openldap, source-postgresql, kyuubi(StatefulSet), weaviate, arangodb, hive-metastore, nessie, kafka(StatefulSet 3 replicas), opensearch, ingress-nginx-controller (ingress mode,
#806) - mesh 미가입 (의도): postgresql (
#790SSL handshake 호환성 — 아래 참조), kube-system 컴포넌트 - 메모리/CPU 부담: linkerd-proxy 평균 ~12 Mi/Pod (예상 50Mi 의 25%)
컴포넌트
| 컴포넌트 | namespace | 역할 |
|---|---|---|
linkerd-destination | linkerd | service discovery + policy fetch |
linkerd-identity | linkerd | workload cert issuance (cert-manager 통합) |
linkerd-proxy-injector | linkerd | mutating webhook (annotation → sidecar 주입) |
linkerd-viz extension | linkerd-viz | metrics-api, web (dashboard), tap |
trust-manager | cert-manager | Root CA bundle 분배 (linkerd-identity-trust-roots ConfigMap) |
sidecar 주입
워크로드에 linkerd.io/inject: enabled annotation 추가:
spec:
template:
metadata:
annotations:
linkerd.io/inject: enabled
config.linkerd.io/proxy-cpu-request: "100m"
config.linkerd.io/proxy-memory-request: "50Mi"
프로토콜별 opaque-ports
Linkerd 의 protocol detection 은 client-first 가정. 서버가 먼저 byte 를 보내는 server-first / binary 프로토콜 은 detection 이 10s timeout 으로 실패 → opaque-ports 명시 필요.
실제 annotation key 는 config.linkerd.io/opaque-ports. 아래 표의 값을 그 key 의 string value 로 사용:
| 프로토콜 | 적용 워크로드 | config.linkerd.io/opaque-ports |
|---|---|---|
| PostgreSQL (server-first SSL) | postgresql, source-postgresql | "5432" (postgresql 은 SSL handshake 충돌로 sidecar 비활성, #790) |
| LDAP / LDAPS | openldap | "389,636" |
| Kafka binary protocol | kafka (broker) | "9092,9093" |
| Hive Metastore Thrift | hive-metastore | "9083" |
| Kyuubi Thrift | kyuubi | "10009" |
| OpenSearch transport | opensearch | "9300" |
전체 적용 예시는 infra/weaviate/deployment.yaml, infra/kafka/base/statefulset.yaml 참조.
NetworkPolicy 호환
mesh 가입 워크로드의 default-deny egress NetworkPolicy 에는 linkerd / linkerd-viz ns 로의 outbound 명시 허용 필수. 누락 시 linkerd-proxy 가 control plane 통신 실패 → PostStartHookError (feedback_linkerd_egress_networkpolicy, #788).
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: linkerd
ports:
- {protocol: TCP, port: 8080} # identity
- {protocol: TCP, port: 8086} # destination
- {protocol: TCP, port: 8090} # policy
- {protocol: TCP, port: 8443} # webhooks
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: linkerd-viz
ports:
- {protocol: TCP, port: 8088} # tap
- {protocol: TCP, port: 8089} # tap admin
검증
linkerd CLI
linkerd --context aks-genos-prod check # control plane health
linkerd --context aks-genos-prod viz check # viz extension
linkerd --context aks-genos-prod viz stat deploy -n gend
Prometheus 통합
infra/monitoring/linkerd-servicemonitor.yaml 의 PodMonitor 3개 등록 (#797):
linkerd-proxy-mesh— sidecar proxy admin (4191), gend/linkerd/linkerd-viz/ingress-nginx nslinkerd-controller— control plane admin-httplinkerd-viz— viz extension admin-http
response_total, request_total 메트릭이 Prometheus 에 누적된다. linkerd-proxy-mesh PodMonitor 는 proxy injector 가 자동으로 부여하는 linkerd.io/proxy-deployment, linkerd.io/proxy-statefulset, linkerd.io/proxy-daemonset, linkerd.io/proxy-job 등 Pod label 을 labelmap 으로 동일 이름의 Prometheus label (deployment, statefulset, ...) 로 변환 → linkerd viz stat 의 워크로드 단위 집계가 동작 (#806).
ingress-nginx mesh 경계 (#806)
외부 트래픽이 gend.genon.ai 로 들어와 백엔드로 라우팅되기 전, ingress-nginx controller 자체를 mesh 의 ingress 모드 로 가입시킨다.
- annotation:
linkerd.io/inject: ingress(≠enabled) - outbound (controller → meshed backend) 가 자동 mTLS 로 보호
- inbound (외부 → nginx, 80/443) 은 nginx 가 직접 TLS 종료 —
skip-inbound-ports: "80,443,8443" - Host 헤더 기반 라우팅을 Linkerd 가 이해 (ingress 모드 동작)
Helm 값: infra/ingress-nginx/values-mesh.yaml.
helm --kube-context aks-genos-prod upgrade ingress-nginx ingress-nginx/ingress-nginx \
--namespace ingress-nginx --version 4.15.1 --reuse-values \
-f infra/ingress-nginx/values-mesh.yaml
검증:
kubectl -n ingress-nginx get pod -l app.kubernetes.io/component=controller \
-o jsonpath='{.items[0].spec.containers[*].name}'
# 기대: linkerd-proxy controller
linkerd --context aks-genos-prod viz stat deploy/gend-api -n gend \
--from deploy/ingress-nginx-controller --from-namespace ingress-nginx
# 기대: ingress-nginx → gend-api 경로의 mTLS 트래픽 통계
ForwardAuth (oauth2-proxy) 흐름은 ClusterIP 통신이라 mesh 경계 변경에도 그대로 동작 (dagster/mlflow/kafka-ui 보호 페이지 302/403 응답 유지).
ServerAuthorization Stage A 강제 (#774)
mlflow, kafka-ui, dagster-webserver 3개 비핵심 워크로드에 대해 meshTLS 인증된 클라이언트만 허용 강제.
infra/linkerd/authz/servers-stage-a.yaml— Server CRD (port + podSelector)infra/linkerd/authz/authorization-stage-a.yaml— ServerAuthorization (meshTLS identity 화이트리스트)
허용 identity:
*.gend.serviceaccount.identity.linkerd.cluster.local— gend ns 내 모든 SA (gend-api, dagster, JupyterHub 등)ingress-nginx.ingress-nginx.serviceaccount.identity.linkerd.cluster.local— 외부 사용자 트래픽의 mesh 진입 SA (#810이후 meshed)
kubectl --context aks-genos-prod apply -f infra/linkerd/authz/
검증:
# allow: 메시 내부 호출 (gend-api → mlflow non-probe)
kubectl exec -n gend deploy/gend-api -- python3 -c \
"import urllib.request; print(urllib.request.urlopen('http://mlflow.gend.svc.cluster.local:5000/api/2.0/mlflow/experiments/list', timeout=5).status)"
# 기대: 404 (mlflow 응답 — authz 통과, route 없음만 의미)
# deny: non-meshed Pod 호출 (kube-system → mlflow). curl -m 으로 자체 timeout.
kubectl run authz-test --rm -i --restart=Never -n kube-system \
--image=curlimages/curl -- \
curl -sS -m 5 -o /dev/null -w "%{http_code}\n" \
http://mlflow.gend.svc.cluster.local:5000/api/2.0/mlflow/experiments/list
# 기대: 403 (Linkerd authz block)
# 사용자 흐름 영향 없음
linkerd --context aks-genos-prod viz authz -n gend deploy/mlflow
# 기대: srv=mlflow-http, authz=mlflow-allow-mesh 행 등장
주의 (probe path 자동 허용): Linkerd 는 /health, /healthz, /readyz, /live* 같은 probe path 를 자동으로 default/probe route 로 분류 → authz 와 무관하게 항상 허용. 따라서 non-meshed Pod 라도 probe path 호출은 200 응답을 받는다. 보안 테스트는 반드시 non-probe path (e.g. /api/...) 로 수행.
Stage B (enforce 확대) — postgresql, kafka, weaviate, arangodb, opensearch 등 핵심 워크로드는 #774 후속 단계.
알려진 제약
PostgreSQL sidecar 비호환 (#790)
PostgreSQL 의 server-first SSL handshake 가 양쪽 mesh proxy 가 동시 처리할 때 negotiation byte ("S"/"N") mangling → ConnectionResetError: Connection reset by peer. opaque-ports: "5432" 만으로 해소 안 됨.
현 prod 상태 (Epic #821 Phase 3a 완료, 2026-05-20):
- PostgreSQL Pod 는 그대로 mesh 밖
- 6 PG client (
gend-api,dagster-daemon,dagster-webserver,mlflow,nessie,hive-metastore) 는 mesh 안 +config.linkerd.io/skip-outbound-ports: "5432"annotation 으로 5432 outbound 만 mesh 우회 - 그 위에 PG 자체 TLS (
verify-full) 활성 — transit 평문 0건, cert chain + hostname 검증
→ 새 PG client 추가 시 driver 별 verify-full 패턴 + 운영 가이드: PostgreSQL TLS. 상세 lesson — feedback_linkerd_postgresql_ssl_corrupt.
Secret 전략: Vault + ESO 중심, 잔여 SealedSecret 병존
prod 의 secret 정본은 Vault (KV v2) 이고, 파드에는 External Secrets Operator
(ClusterSecretStore vault-backend, k8s auth role eso-reader) 가 동기화한 K8s Secret 으로
도달한다. SealedSecret 은 ESO 이관 전 잔여분으로 병존하며 Epic #1107 로 청산 예정이다.
Vault Agent Injector 는 미설치다 — 이 부분은 지금도 유효하다(실측: prod agent-injector
파드 0개, 리포 전체 /vault/secrets 마운트 0건). 즉 mesh 사이드카와 경합하는 주입 경로는
애초에 없다.
:::note 이 절의 옛 서술 정정 (2026-08-08)
기존에는 "K8s Secret + SealedSecret 으로 통일, Vault 는 운영 부담이 커 별도 epic(#812)"
이라고 적혀 있었다. Vault 는 그 뒤 실제로 도입됐다 — #1172 로 2026-05-28 에 prod
설치(Raft 3-node + Azure KV auto-unseal), 현재 ESO 6/6 이 그 Vault 에서 동기화 중이며
감사 장치도 활성화돼 있다. 근거로 인용된 #812 는 2026-05-16 에 종결됐다.
★ "미도입" 서술은 도입되는 순간 거짓이 되는데, 아무도 되돌아와 고치지 않는다. :::
gend-api의 DB credential:postgresql-secret의 password 를GEND_DB_PASSWORDenv 로 직접 주입 →GEND_DATABASE_URL에 변수 치환으로 합성 (infra/helm/gend-api/deployment.yaml).- 다른 mesh 워크로드도 동일 패턴 (
envFrom.secretRef또는env[].valueFrom.secretKeyRef+ SealedSecret). - GitOps 친화: SealedSecret manifest 는 Git 에 commit 가능 (private key 가 cluster 에 있어야만 복호화).
롤백 (개별 워크로드)
Deployment / StatefulSet 의 Pod template annotation 을 disabled 로 patch:
# Deployment
kubectl --context aks-genos-prod patch deploy <name> -n gend --type=merge -p '{
"spec":{"template":{"metadata":{"annotations":{"linkerd.io/inject":"disabled"}}}}
}'
# StatefulSet (kafka, kyuubi 등)
kubectl --context aks-genos-prod patch statefulset <name> -n gend --type=merge -p '{
"spec":{"template":{"metadata":{"annotations":{"linkerd.io/inject":"disabled"}}}}
}'
Pod restart (StatefulSet 는 ordered rolling) 시 sidecar 제거된 상태로 복귀. 통신 호환성:
- 해당 Pod 가 다른 meshed Pod 의 server 로 호출 시: meshed proxy 가 평문 client 를 받아들임 (server-side passthrough)
- 해당 Pod 가 외부 (non-meshed) 또는 meshed client → 양쪽 모두 평문 통신
즉 mTLS 효과만 잃을 뿐 connectivity 자체는 정상.
Refs
- 설계 문서:
docs/DESIGN_LINKERD_MTLS.md - 설치 가이드:
infra/linkerd/README.md - Phase issue:
#519epic,#773(sidecar rollout),#775(Phase 13.4 외부 통합),#774(Phase 13.3 AuthZ) - Hotfix lessons:
#788(egress NetworkPolicy),#790(PG SSL outage),#796(Dagster GraphQL path-prefix)