{"id":"IL-0197","org":"Palark","date":"2021-05-27","docs":[],"title":{"en":"Moving a three-node Elasticsearch StatefulSet to new persistent volumes split the cluster in two, because the Helm chart hardcoded cluster.initial_master_nodes to elasticsearch-0","ko":"Elasticsearch StatefulSet 을 새 퍼시스턴트 볼륨으로 옮기던 중 빈 볼륨으로 다시 뜬 elasticsearch-0 이 혼자 새 클러스터를 만들어 스플릿 브레인이 났다."},"domain":["kubernetes"],"impact":{"scope":"A three-node Elasticsearch cluster in the kibana-production namespace of Palark's non-critical internal infrastructure; new data was written to two separate clusters until the writing workers were stopped, and those workers stayed down during the repair","users":"unknown","region":"unknown"},"review":{"written_by":"session","reviewed_at":null,"reviewed_by":null},"lessons":{"en":"A setting that only matters when a cluster first starts, such as Elasticsearch's cluster.initial_master_nodes, can sit in a Helm chart template rather than in the ConfigMap that was reviewed. When a pod named in it starts with an empty data directory, it can bootstrap a new cluster beside the existing one.\n\nThe author's conclusion is that a migration plan like this one breaks on any surprise and needs a plan B ready. Before running it in a more important environment, rehearse it in a test environment configured like production, and either schedule downtime or move the load to another cluster: stop the writers, take a fresh backup, then move the data.","ko":"운영 데이터를 옮길 때는 설정 오류, 데이터센터 장애, 네트워크 단절처럼 무언가 잘못될 수 있다는 전제로 준비해야 한다. 이번에 쓴 다운타임 없는 절차는 예상하지 못한 문제에 약했다. 팀은 ConfigMap 을 꼼꼼히 확인했지만 Helm 차트 템플릿이 StatefulSet 에 넣는 환경 변수는 보지 않았다. cluster.initial_master_nodes 가 파드 하나로 고정된 상태에서 그 파드가 빈 볼륨으로 다시 뜨면 기존 클러스터에 합류하지 않고 새 클러스터를 만든다."},"related":[],"sources":[{"url":"https://palark.com/blog/failure-stories-elasticsearch-migration-within-kubernetes/","kind":"blog","license":"unknown","archived":null}],"summary":{"en":"Palark was upgrading a non-critical part of its internal production infrastructure, used for testing technical solutions, from Kubernetes 1.15. Its persistent volumes came from the Rook v0.9 operator, whose Helm release contained resources with deprecated API versions, so instead of upgrading Rook in the running cluster the team moved data by hand to storage classes not managed by Rook. One of the workloads was a three-node Elasticsearch cluster deployed as a StatefulSet from a Helm chart.\n\nThe plan was to change the storage class in the chart's volumeClaimTemplates, delete the StatefulSet without cascading, redeploy it with six replicas, drain all shards from the three old nodes, and then recreate replicas 0, 1 and 2 one by one on new PVCs. The chart's StatefulSet template, not the reviewed ConfigMap, also set the environment variable cluster.initial_master_nodes to the name of pod 0. That setting decides the master-eligible nodes when a cluster starts for the first time. When elasticsearch-0 started with an empty volume, it began bootstrapping a new cluster and ignored the existing master in elasticsearch-2, leaving two clusters that both received writes.\n\nThe team first scaled the three deployments of workers that write to Elasticsearch down to zero, accepting daytime downtime over growing data loss. It then listed elasticsearch-0, -1 and -2 under initial_master_nodes in the ConfigMap and removed the environment variable from the StatefulSet, which started a rolling update. Unsure whether master election would be safe during a fast rolling update, the author saved and deleted the elasticsearch discovery Service and deleted the PVC of pod 0, which left the nodes unable to see each other (`Open Distro Security not initialized.`). After setting initial_master_nodes to elasticsearch-3, -4 and -5, creating an empty PVC for pod 0, deleting all six pods and restoring the Service, the six nodes formed one cluster and no index was in a state other than green. The author later found the election would have gone fine without these extra steps.","ko":"Palark 는 내부 운영 인프라 가운데 중요도가 낮은 부분의 Kubernetes 를 1.15 에서 올리려 했다. 이 부분의 퍼시스턴트 볼륨은 Rook operator v0.9 가 만들었다. 이 오래된 operator 의 Helm 릴리스에 폐기된 API 버전의 리소스가 들어 있어 클러스터를 업그레이드할 수 없었다. 팀은 실행 중인 클러스터에서 Rook 을 올리지 않고 손으로 걷어 내기로 했고, 데이터를 Rook 이 관리하지 않는 StorageClass 로 옮기고 있었다.\n\nkibana-production 네임스페이스의 3노드 Elasticsearch 는 다운타임 없이 옮기기로 했다. 팀은 차트의 volumeClaimTemplates 에서 StorageClass 를 rbd 에서 high-speed 로 바꾸고, StatefulSet 을 --cascade=false 로 지운 뒤 다시 배포해 레플리카를 6개로 늘렸다. 리밸런싱 한도를 올리고 cluster.routing.allocation.exclude._ip 로 옛 노드 세 개에서 샤드를 모두 빼냈다. 그다음 레플리카 2, 1, 0 의 PVC 와 파드를 하나씩 지우고 high-speed PVC 를 손으로 만들어 새 볼륨을 연결했다.\n\nelasticsearch-2 와 elasticsearch-1 은 기존 클러스터에 다시 합류했지만 elasticsearch-0 은 합류하지 않았다. 차트의 StatefulSet 이 cluster.initial_master_nodes 를 0번 파드 하나로 고정해 두었기 때문이다. 이 값은 클러스터를 처음 만들 때 마스터 후보를 정한다. 0번 파드가 빈 볼륨으로 시작하자 클러스터 부트스트랩이 새로 시작됐고, elasticsearch-2 에 있던 기존 마스터는 무시됐다.\n\n팀은 데이터를 쓰는 워커를 멈춰 데이터가 두 클러스터로 갈라지는 것을 막았다. 파드 라벨을 떼어 Service 에서 빼는 방법은 쓰지 않았다. Elasticsearch 는 이 Service 로 클러스터 멤버를 찾기 때문에 파드를 한 번 빼면 다시 합류시킬 수 없었다. 그 뒤 initial_master_nodes 설정을 고치고 디스커버리 Service 와 0번 PVC 를 지웠다가 다시 만드는 순서로 노드 6개를 한 클러스터로 모았다. 마스터 선출은 문제없이 끝났고 green 이 아닌 인덱스는 0개였다."},"trigger":{"en":"Deleting the elasticsearch-0 pod and its PVC so that it restarted on an empty volume of the new StorageClass.","ko":"옛 PVC 와 파드를 지운 뒤 elasticsearch-0 이 새 StorageClass 의 빈 볼륨을 받아 다시 시작했다."},"severity":"unknown","symptoms":["split-brain"],"timeline":{"detected_after":null,"resolved_after":null},"date_kind":"published","detection":{"en":"The engineer running the migration checked `_cat/nodes` after elasticsearch-0 restarted and saw it alone in a cluster of its own.","ko":"마지막으로 0번 파드를 교체한 뒤 엔지니어가 elasticsearch-0 과 elasticsearch-1 안에서 각각 `_cat/nodes` 를 조회했다. 두 결과가 서로 다른 노드 목록과 다른 마스터를 보여 스플릿 브레인을 알아챘다."},"components":["elasticsearch-opensearch","persistent-volume","helm"],"root_cause":"human-operation","remediation":[{"text":{"en":"scaled the three deployments of workers writing to Elasticsearch down to zero","ko":"데이터를 실시간으로 쓰는 워커의 디플로이먼트 세 개를 0으로 줄여 새 데이터가 갈라진 클러스터로 들어가지 않게 했다."}},{"text":{"en":"listed elasticsearch-0, elasticsearch-1 and elasticsearch-2 under initial_master_nodes in the elasticsearch ConfigMap and removed the cluster.initial_master_nodes environment variable from the StatefulSet","ko":"ConfigMap 의 initial_master_nodes 를 elasticsearch-0, elasticsearch-1, elasticsearch-2 로 바꾸고 StatefulSet 에서 cluster.initial_master_nodes 환경 변수를 지웠다."}},{"text":{"en":"saved and deleted the elasticsearch discovery Service and deleted the PVC of elasticsearch-0 while the rolling update ran","ko":"롤링 업데이트가 도는 동안 디스커버리용 elasticsearch Service 를 YAML 로 저장한 뒤 지우고, 0번 파드의 PVC 도 지웠다."}},{"text":{"en":"set initial_master_nodes to elasticsearch-3, elasticsearch-4 and elasticsearch-5, created an empty PVC for elasticsearch-0 and deleted all six pods","ko":"ConfigMap 의 initial_master_nodes 를 elasticsearch-3, elasticsearch-4, elasticsearch-5 로 바꾸고, 0번 파드의 빈 PVC 를 만든 뒤 파드 6개를 모두 지워 다시 띄웠다."}},{"text":{"en":"recreated the discovery Service from the saved manifest, after which the six nodes formed one cluster with every index green","ko":"저장해 둔 YAML 로 Service 를 다시 만들었다. 노드 6개가 한 클러스터로 모였고 green 이 아닌 인덱스는 0개였다."}}],"action_items":[{"text":{"en":"Restore the original initial_master_nodes values in the ConfigMap and restart all pods again","ko":"ConfigMap 의 initial_master_nodes 를 원래 값으로 되돌리고 모든 파드를 한 번 더 재시작한다."}},{"text":{"en":"Move all shards to nodes 0, 1 and 2 and scale the cluster down from six nodes to three","ko":"모든 샤드를 0, 1, 2번 노드로 옮기고 클러스터를 노드 6개에서 3개로 줄인다."}},{"text":{"en":"Commit all the manual changes to the repository","ko":"손으로 바꾼 내용을 모두 저장소에 커밋한다."}}],"contributing":["assumption-unverified"],"recurrence_of":null,"symptoms_text":{"en":"After the last old Elasticsearch pod, elasticsearch-0, was deleted and came back on a new, empty persistent volume, `_cat/nodes` run inside that pod listed a single node: elasticsearch-0 itself, marked as master. The same command run inside elasticsearch-1 listed the other five nodes, elasticsearch-1 to elasticsearch-5, with elasticsearch-2 as master.\n\nThe Elasticsearch cluster had split into two separate clusters, and new data was being written at random to one or the other. This happened in the middle of a planned migration of the three-node cluster in the kibana-production namespace from volumes of the rbd StorageClass to the high-speed StorageClass, done without downtime. The StatefulSet had been deleted with --cascade=false, redeployed with the new storage class and scaled to six replicas, shards had been moved off the three old nodes with cluster.routing.allocation.exclude._ip, and then the pods and PVCs of replicas 2, 1 and 0 were deleted one at a time. Replicas 2 and 1 rejoined the cluster on their new volumes; replica 0 did not.\n\nThe ConfigMap had been checked carefully before the migration and still looked correct. Taking elasticsearch-0 out of the Service by removing its label was considered as a way to stop writes to it, and rejected: the members find each other through the same Service (the DISCOVERY_SERVICE variable feeds discovery.zen.ping.unicast.hosts), so the pod could not have rejoined the cluster afterwards.","ko":"옛 노드 세 개를 하나씩 새 StorageClass 의 볼륨으로 바꾸다가 마지막으로 elasticsearch-0 파드를 지우고 다시 띄웠다. elasticsearch-0 안에서 `_cat/nodes` 를 조회하자 자기 자신 하나만 마스터로 나왔다.\n\n같은 시각 elasticsearch-1 안에서 같은 조회를 하면 elasticsearch-1, elasticsearch-2, elasticsearch-3, elasticsearch-4, elasticsearch-5 다섯 노드가 보였고 마스터는 elasticsearch-2 였다. 클러스터가 둘로 갈라진 스플릿 브레인 상태였다. 새로 들어오는 데이터는 두 클러스터 중 아무 쪽에나 무작위로 쓰이고 있었다.\n\n엔지니어는 먼저 설정을 다시 확인했지만 ConfigMap 에는 이상이 없었다. 원인은 Helm 차트의 es-data-statefulset.yaml 에 있었다. 차트가 StatefulSet 에 cluster.initial_master_nodes 환경 변수를 0번 파드 이름 하나로 박아 넣고 있었다.\n\n복구 중에 디스커버리 Service 와 0번 파드의 PVC 를 지운 상태로 롤링 업데이트가 끝나자 elasticsearch-0 은 PVC 가 없어 Pending 이 됐다. 나머지 노드는 서로를 찾지 못했고, elasticsearch-1 에서 `_cat/nodes` 를 조회하면 Open Distro Security not initialized. 라는 메시지가 나왔다."},"updated_at":"2026-11-10T00:00:40.720149Z","url":"https://benchlet.com/incidentlib/app/cases/IL-0197","org_slug":"palark"}