원인은 JupyterLab/Spark의 인증서 자체 문제라기보다, ingress-nginx controller 내부의 TLS 인증서 캐시 메모리 부족일 가능성이 큽니다.
해당 메시지는 ingress-nginx의 Lua 동적 설정 처리에서 /configuration/servers 요청을 처리하면서 certificate_data shared dictionary에 인증서를 저장하려고 했는데, 공간이 부족해서 기존 LRU 항목을 밀어낸 상황과 일치합니다. ingress-nginx 코드에서도 certificate_data:set() 후 공간 부족으로 항목이 강제 제거되면 "certificate_data dictionary is full, LRU entry has been removed..."를 로깅하도록 되어 있습니다. (GitHub) OpenResty의 ngx.shared.DICT:set()도 shared memory가 부족하면 기존 항목을 LRU 방식으로 제거할 수 있고, 이때 forcible=true가 반환된다고 설명합니다. (OpenResty Reference)
lua-shared-dicts 증설현재 가장 직접적인 해결책은 ingress-nginx ConfigMap에 아래 값을 추가하거나 늘리는 것입니다.
data:
lua-shared-dicts: "certificate_data: 100, certificate_servers: 20, ocsp_response_cache: 20"
certificate_data는 실제 인증서/키 데이터를 저장하는 영역이고, certificate_servers는 hostname과 인증서 매핑 쪽에 사용됩니다. ingress-nginx 기본값은 코드 기준으로 certificate_data: 20480KB, 즉 약 20MiB, certificate_servers: 5120KB, ocsp_response_cache: 5120KB 수준입니다. 대규모 JupyterLab/Spark 사용자별 Ingress, namespace별 TLS Secret, 긴 인증서 체인 환경에서는 이 값이 쉽게 부족할 수 있습니다. (GitHub) ingress-nginx 공식 ConfigMap 문서에서도 lua-shared-dicts로 기본 Lua shared dictionary 크기를 커스터마이즈할 수 있고, 예시로 certificate_data: 100을 제시합니다. 단위를 생략하면 MB로 해석됩니다. (Kubernetes)
운영 환경에서는 우선 아래 정도를 권장합니다.
lua-shared-dicts: "certificate_data: 100, certificate_servers: 20, ocsp_response_cache: 20"
TLS Secret이 수천 개 이상이거나 namespace마다 동일한 wildcard 인증서를 복사해서 쓰는 구조라면 다음처럼 더 키우는 것도 고려할 수 있습니다.
lua-shared-dicts: "certificate_data: 150, certificate_servers: 50, ocsp_response_cache: 50"
단, 현재 ingress-nginx 코드 기준으로 dictionary 하나의 최대 허용값은 204800KB, 즉 약 200MiB입니다. 이보다 크게 설정하면 무시될 수 있습니다. (GitHub)
ConfigMap 이름은 환경마다 다를 수 있지만 보통 아래 중 하나입니다.
kubectl get cm -A | grep -i ingress
예를 들어 namespace가 ingress-nginx, ConfigMap이 ingress-nginx-controller라면:
kubectl -n ingress-nginx patch configmap ingress-nginx-controller \
--type merge \
-p '{"data":{"lua-shared-dicts":"certificate_data: 100, certificate_servers: 20, ocsp_response_cache: 20"}}'
Helm으로 관리 중이면 values에 넣는 방식이 더 안전합니다.
controller:
config:
lua-shared-dicts: "certificate_data: 100, certificate_servers: 20, ocsp_response_cache: 20"
적용 후에는 ingress controller를 reload/restart해야 합니다. Lua shared dict는 NGINX shared memory zone이라 런타임에서 자유롭게 확장되는 구조가 아니며, 설정 변경 후 reload가 필요합니다. OpenResty 쪽 설명에서도 shared dict 공간이 부족하면 NGINX 설정을 수정하고 reload해야 한다고 설명합니다. (API7.ai)
kubectl -n ingress-nginx rollout restart deployment/ingress-nginx-controller
kubectl -n ingress-nginx rollout status deployment/ingress-nginx-controller
운영 중이면 ingress-nginx controller replica가 2개 이상인지 먼저 확인하세요.
kubectl -n ingress-nginx get deploy ingress-nginx-controller
1개 replica만 있다면 restart 중 짧은 영향이 있을 수 있으므로, 가능하면 2개 이상으로 운영하는 것이 좋습니다.
설정 반영 여부는 controller pod 안의 NGINX 설정에서 확인할 수 있습니다.
POD=$(kubectl -n ingress-nginx get pod \
-l app.kubernetes.io/component=controller \
-o jsonpath='{.items[0].metadata.name}')
kubectl -n ingress-nginx exec "$POD" -- nginx -T 2>/dev/null \
| grep -E 'lua_shared_dict +(certificate_data|certificate_servers|ocsp_response_cache)'
예상 출력은 아래와 비슷해야 합니다.
lua_shared_dict certificate_data 100m;
lua_shared_dict certificate_servers 20m;
lua_shared_dict ocsp_response_cache 20m;
이후 로그에서 아래 메시지가 사라지는지 확인합니다.
kubectl -n ingress-nginx logs -l app.kubernetes.io/component=controller --since=30m \
| egrep -i 'certificate_data dictionary is full|certificate_servers dictionary is full|certificate not found|fake certificate'
JupyterLab/Spark 접속 도메인에 실제 올바른 인증서가 내려오는지도 확인합니다.
openssl s_client -connect <HOST>:443 -servername <HOST> -showcerts </dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates
간헐적으로 Kubernetes Ingress Controller Fake Certificate, default cert, self-signed cert가 보이면 이 문제가 계속 남아 있는 것입니다.
JupyterLab, Spark UI, Spark History Server, 사용자별 sandbox처럼 사용자/앱별 hostname이 많고 Ingress가 자주 생성·삭제되는 환경에서는 ingress-nginx가 동적으로 server/certificate 설정을 갱신합니다. 이때 certificate_data 캐시가 부족하면 일부 인증서 항목이 LRU로 밀려나고, 특정 SNI 요청 시 올바른 인증서를 못 찾거나 default/fake certificate를 내보낼 수 있습니다. 그래서 모든 서비스가 항상 실패하는 게 아니라, 특정 사용자·특정 hostname·특정 시점에만 인증서 오류가 간헐적으로 보일 수 있습니다.
증설은 즉시 조치이고, 근본적으로는 Ingress별/namespace별 TLS Secret 중복을 줄이는 것이 좋습니다.
특히 사용자별 JupyterLab/Spark가 다음처럼 구성되어 있다면 문제가 커집니다.
user-a.ns-a.example.com -> ns-a/tls-secret
user-b.ns-b.example.com -> ns-b/tls-secret
user-c.ns-c.example.com -> ns-c/tls-secret
...
가능하면 아래 구조를 권장합니다.
*.lab.example.com
*.spark.example.com
*.notebook.example.com
그리고 ingress-nginx controller에 wildcard 인증서를 default SSL certificate로 설정합니다. ingress-nginx는 --default-ssl-certificate=<namespace>/<secret> 옵션을 통해 기본 인증서를 지정할 수 있고, 이 옵션이 없으면 self-signed certificate를 사용한다고 문서화되어 있습니다. 또한 tls: 섹션은 두되 secretName을 생략하면 default certificate를 사용하면서 HTTPS redirect를 강제할 수 있습니다. (Kubernetes)
예시:
controller:
extraArgs:
default-ssl-certificate: "ingress-nginx/lab-wildcard-tls"
각 사용자 Ingress는 가능하면 개별 secretName을 제거합니다.
spec:
tls:
- hosts:
- user-a.lab.example.com
rules:
- host: user-a.lab.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: jupyterlab
port:
number: 8888
이렇게 하면 인증서 데이터 수가 사용자 수만큼 증가하지 않고, hostname 매핑 중심으로만 증가하므로 certificate_data 압박이 크게 줄어듭니다.
TLS host 개수:
kubectl get ingress -A -o json \
| jq '[.items[].spec.tls[]?.hosts[]?] | unique | length'
Ingress에서 참조하는 TLS Secret 개수:
kubectl get ingress -A -o json \
| jq '[.items[] as $i
| ($i.spec.tls[]?
| select(.secretName != null)
| "\($i.metadata.namespace)/\(.secretName)")]
| unique | length'
TLS Secret 전체 크기 대략 확인:
kubectl get secret -A -o json \
| jq '[.items[]
| select(.type=="kubernetes.io/tls")
| ((.data["tls.crt"] // "" | @base64d | length)
+ (.data["tls.key"] // "" | @base64d | length))]
| add / 1024 / 1024'
이 값이 이미 10~20MiB 근처이거나 그 이상이면 기본 certificate_data 20MiB는 부족할 가능성이 큽니다. 실제 shared dict에는 key, metadata, Lua/OpenResty 관리 오버헤드도 들어가기 때문에 단순 PEM 크기보다 여유를 크게 잡아야 합니다.
우선 아래 설정을 적용하는 것이 1차 해결책입니다.
lua-shared-dicts: "certificate_data: 100, certificate_servers: 20, ocsp_response_cache: 20"
그 후에도 같은 로그가 남으면 certificate_data를 150~200까지 올리되, 200MiB 한계에 가까워진다면 단순 증설보다 wildcard/default SSL certificate 사용, per-user TLS Secret 제거, IngressClass/ingress controller 분리를 검토하는 것이 맞습니다.
==
이 에러는 nginx-ingress controller (ingress-nginx) 의 Lua 공유 딕셔너리(certificate_data)가 가득 찼을 때 발생합니다.
certificate_Data dictionary is full → LRU 방식으로 오래된 인증서 항목 제거
→ 해당 인증서가 필요한 요청에서 간헐적 TLS/인증서 에러 발생
lua-shared-dicts 용량 증가 (권장)ingress-nginx ConfigMap을 수정하여 딕셔너리 크기를 늘립니다.
kubectl edit configmap ingress-nginx-controller -n ingress-nginx
apiVersion: v1
kind: ConfigMap
metadata:
name: ingress-nginx-controller
namespace: ingress-nginx
data:
# 기존 값 확인 후 certificate_data 크기를 늘림
lua-shared-dicts: "certificate_data:20,configuration_data:5"
# ↑ 기본값 20MB → 상황에 따라 40~100MB로 증가
적용 후 ingress-nginx pod 재시작:
kubectl rollout restart deployment ingress-nginx-controller -n ingress-nginx
실제 얼마나 차 있는지 확인합니다.
# ingress-nginx pod 이름 확인
kubectl get pods -n ingress-nginx
# nginx 상태 엔드포인트로 lua dict 사용량 확인
kubectl exec -n ingress-nginx <pod-name> -- \
curl -s http://localhost:10246/configuration | python3 -m json.tool
또는 nginx 내부에서 직접:
kubectl exec -n ingress-nginx <pod-name> -- \
/dbg backends all 2>/dev/null | head -50
certificate_data 분리 최적화Ingress 수가 100개 이상인 경우, 아래 설정도 함께 적용:
data:
lua-shared-dicts: "certificate_data:50,configuration_data:10,global_throttle_conn:1,global_throttle_conn_rps:1"
# 추가로 worker 프로세스 튜닝
worker-processes: "auto"
worker-cpu-affinity: "auto"
구버전(< 1.3.0)은 이 버그가 있어 버전 업이 근본 해결책이 될 수 있습니다.
# 현재 버전 확인
kubectl exec -n ingress-nginx <pod-name> -- /nginx-ingress-controller --version
1. lua-shared-dicts 값 확인 (현재 설정값)
↓
2. certificate_data를 40MB 이상으로 증가
↓
3. ingress-nginx rollout restart
↓
4. JupyterHub/Spark에서 인증서 에러 재현 여부 모니터링
↓
5. 에러 지속 시 → 80~100MB로 추가 증가 or 버전 업그레이드
| 환경 | certificate_data 권장값 |
|---|---|
| Ingress 50개 이하 | 20MB (기본값) |
| Ingress 50~200개 | 40~60MB |
| Ingress 200개 이상 | 80~100MB |
| JupyterHub + Spark 멀티 도메인 | 최소 50MB 이상 |
현재 Ingress 수를 확인하려면:
kubectl get ingress -A | wc -l
===
이 메시지는 Kubernetes Ingress-NGINX 내부의 Lua 스크립트(configuration.lua)가 SSL/TLS 인증서 데이터를 캐싱하는 공유 메모리(Shared Dictionary) 공간인 certificate_data가 가득 찼을 때 발생합니다.
메모리가 부족해지자 최신 요청을 저장하기 위해 LRU(Least Recently Used, 가장 오랫동안 사용되지 않은) 알고리즘에 의해 기존 인증서 엔트리를 강제로 밀어내고(Evict) 있는 상태입니다. 이 과정에서 필요한 인증서가 순간적으로 유실되거나, NGINX 워커가 갱신 주기 타이밍을 놓쳐 jupyteralb, spark 등 내부 통신 시 간헐적인 인증서 에러(Expired, Fake Certificate, 또는 Handshake Failure)를 유발하게 됩니다.
이 문제는 관리하는 Ingress 객체나 도메인, Secret(인증서)의 개수가 많아질 때 주로 발생하며, 아래의 방법으로 명확하게 해결할 수 있습니다.
lua-shared-dicts 설정 확장Ingress-NGINX Controller의 ConfigMap을 수정하여 인증서 데이터를 담는 Lua 공유 메모리 크기를 늘려주어야 합니다. 기본값은 보통 수 MB 수준으로 작게 잡혀 있습니다.
values.yaml 파일에 아래 내용을 추가하거나 업데이트한 후 Helm Upgrade를 진행합니다.
controller:
config:
lua-shared-dicts: "certificate_data: 100" # 기본 단위를 대폭 늘려 (예: 100MB) 공간 확보
💡 일반적으로
certificate_data: 50또는100정도로 지정하면 수천 개의 도메인 및 인증서 캐시를 안정적으로 수용할 수 있습니다.
수정 후 적용:
helm upgrade -n <ingress-namespace> <release-name> ingress-nginx/ingress-nginx -f values.yaml
만약 Manifest 파일로 배포했거나 빠르게 임시 조치를 취해야 한다면, Ingress Controller가 참조하는 ConfigMap(보통 이름이 ingress-nginx-controller)을 직접 편집합니다.
kubectl edit cm -n <ingress-namespace> ingress-nginx-controller
data: 섹션 아래에 lua-shared-dicts 설정을 주입합니다.
apiVersion: v1
kind: ConfigMap
metadata:
name: ingress-nginx-controller
namespace: ingress-nginx
data:
# 기존 설정들이 배치되어 있는 곳에 아래 라인 추가
lua-shared-dicts: "certificate_data: 100"
저장 후 빠져나오면 Ingress Controller가 변경 사항을 감지하여 동적으로 반영하거나 Pod가 롤링 리스타트됩니다.
lua-shared-dicts사이즈를 변경하면, NGINX 메모리 구조가 재조정되면서 기존 캐시에 들어있던 모든 인증서 데이터가 일시적으로 퍼지(Purge/초기화)될 수 있습니다.
이 경우 새롭게 인증서 데이터를 채워 넣는 약 수 분~20분 동안, 외부 요청에 대해 Ingress가 임시 인증서(Fake Certificate)를 반환하여 서비스 단절이나 SSL 경고가 발생할 가능성이 있습니다. 따라서 반드시 트래픽이 적은 유지보수 시간대(정비 시간)에 작업을 진행하시는 것을 강력히 권장합니다.