{"id":"IL-0060","org":"Reddit","date":"2023-03-14","docs":[],"title":{"en":"Kubernetes 1.24 upgrade removed the master node label that Calico route reflectors selected on, dropping all pod routes and taking Reddit down for 314 minutes","ko":"쿠버네티스 1.24 업그레이드로 node-role.kubernetes.io/master 레이블이 사라지자 이 레이블에 기대던 Calico 라우트 리플렉터가 동작을 멈춰 레거시 클러스터의 네트워크가 모두 끊기고 Reddit 이 314분 동안 멈췄다"},"domain":["kubernetes","aws","networking"],"impact":{"scope":"most of reddit.com; the legacy cluster was down while modern service API layers stayed up","users":"unknown","region":"unknown"},"review":{"written_by":"session","reviewed_at":null,"reviewed_by":null},"lessons":{"en":"Configuration that exists only in a live cluster, edited by hand through a special tool, can be forgotten, as the route reflector setup was, and then break on an upgrade nobody connects it to. A Kubernetes upgrade cannot be reverted without a restore, so the restore procedure has to be kept current and practised. Consistent, codified clusters are what make upgrades testable; bespoke clusters keep producing outages.","ko":"쿠버네티스 버전과 구성 요소 업그레이드는 여전히 위험하다. 미리 테스트하고 검증해도 이번처럼 처음 보는 방식으로 깨질 수 있다.\n\n쿠버네티스에는 공식 다운그레이드 절차가 없어, 롤백하는 방법은 백업에서 복원하는 것뿐이다. 복원 절차가 환경 변화를 따라가지 못하면 실제 장애 때 그 자리에서 고쳐 써야 하고, 복원을 결정하기까지 오래 망설이게 된다. 이번 복원은 결국 성공했지만 절차는 개선해야 한다.\n\n코드로 남기지 않은 설정은 아는 사람이 떠나면 아무 기록도 남지 않는다. 라우트 리플렉터 설정이 어디에도 커밋되어 있지 않아 장애 중에 누구도 의심하지 못했다.\n\n지표가 클러스터 안에서만 수집되면 클러스터가 망가질 때 함께 사라진다. 이번에는 로그 수집 에이전트가 영향을 받지 않아 원인을 추적할 수 있었다.\n\n클러스터마다 다른 맞춤형 구성이 쌓이면 장애가 잦아진다. 직접 원인은 노드 레이블이었지만 근본 원인은 일관성 부족이었다."},"related":[],"sources":[{"url":"https://www.reddit.com/r/RedditEng/comments/11xx5o0/you_broke_reddit_the_piday_outage/","kind":"blog","license":"unknown","archived":"https://web.archive.org/web/20260826130053/https://www.reddit.com/r/RedditEng/comments/11xx5o0/you_broke_reddit_the_piday_outage/"}],"summary":{"en":"Reddit's cluster for the legacy stack was one of several bespoke clusters built with kubeadm. Years earlier, a predecessor of the Compute team had configured Calico, the cluster's network plugin, to use route reflectors: instead of every node peering with every other node in a full mesh, the control plane nodes act as reflectors and all other nodes peer only with them. The route reflector nodeSelector and peerSelector matched the label node-role.kubernetes.io/master. Kubernetes renamed the role to control-plane in the 1.20 series and removed the master label in 1.24. When the first control plane node was upgraded, calico-node across the cluster began dropping routes to it, as expected, but it also began dropping the routes for every other node; that happened two seconds before the site broke. Cluster networking failed, and with it most user flows. The route reflector setup could only be edited through Calico's own CLI, was committed nowhere, and everyone who knew about it had left; it was only found during the postmortem.\n\nKubernetes has no supported downgrade path, so rolling back meant restoring the control plane from the backup taken at the start of the upgrade. The restore procedure was years old, written for an end-of-life Kubernetes version and for Docker rather than CRI-O, and had never been run against a production cluster. After more than two hours of trying to fix forward, the team made the call to restore. All worker nodes were terminated first, which took about 20 minutes of API calls. The procedure was rewritten live as commands failed, and the restore to the first control plane node succeeded. The cluster autoscaler came back and was switched off to regain control. Launching the two replacement control plane nodes was delayed because AWS had no capacity for that instance type. The new nodes then could not join, because the backup had been taken on a different control plane node from the one restored, so the TLS certificates did not match the hostname; the team generated new certificates.\n\nTraffic was brought back gradually, at 1%, 5%, 10%, 20%, 35%, 55%, 80% and 100%, to avoid a thundering herd against cold caches and scaled-down services, and some legacy services that had been stopped by hand were started again. The outage lasted 314 minutes. Reddit's modern service API layers stayed up, but the legacy cluster sits on the most critical node of the dependency graph.","ko":"2023년 3월 14일 Reddit 의 Compute 팀이 레거시 스택을 돌리는 클러스터를 쿠버네티스 1.23 에서 1.24 로 올리기 시작했다. 2분 뒤 사이트가 멈췄고 장애는 314분 동안 이어졌다. 최신 서비스의 API 계층은 살아 있었지만, 의존 관계에서 가장 중요한 레거시 클러스터가 내려가 대부분의 사용자 흐름이 영향을 받았다. 이 클러스터는 kubeadm 으로 직접 만든 맞춤형 클러스터였다.\n\n가장 먼저 떠올린 것은 롤백이었다. 하지만 쿠버네티스는 업그레이드 중에 스키마와 데이터 마이그레이션을 자동으로 하기 때문에 공식 다운그레이드 절차가 없다. 다운그레이드하려면 백업에서 복원해야 했다. 업그레이드 절차에 백업이 들어 있었지만, 복원 절차는 몇 년 전에 쓴 것이고 운영 클러스터에서 실행해 본 적이 없었다. 복원에 몇 시간이 걸릴 것으로 보여 팀은 우선 되돌리지 않고 지금 상태에서 문제를 고치기로 했다. 한 시간쯤 지나 Compute 팀 일부가 따로 복원 준비를 시작했다.\n\n두 시간이 넘도록 원인을 찾지 못하자 팀은 백업에서 복원하기로 했다. 복원하면 기존 워커 노드는 어차피 쓸 수 없게 되므로 워커 노드를 모두 종료했다. 가장 큰 클러스터여서 API 호출이 다 끝나는 데 20분쯤 걸렸다. 컨트롤 플레인 노드 두 대를 종료하고, 남은 한 대의 구성 요소를 다운그레이드하고, 그 노드에 데이터를 복원했다. 복원 절차는 지원이 끝난 쿠버네티스 버전과 Docker 를 기준으로 쓰여 있어 명령 문법과 인자가 맞지 않았고, 현장에서 고쳐 써야 했다. 첫 노드가 살아나자 클러스터 오토스케일러가 노드를 띄우기 시작했다. 네트워크가 돌아왔다는 신호였지만 팀은 상태를 정리할 시간을 벌려고 오토스케일러를 껐다.\n\n나머지 컨트롤 플레인 노드 두 대를 띄울 때는 AWS 에 해당 인스턴스 유형의 용량이 없어 더 늦어졌다. 노드가 뜬 뒤에도 첫 노드의 etcd 에 연결하지 못해 클러스터에 합류하지 못했다. 백업은 어느 컨트롤 플레인 노드에서나 뜰 수 있었지만 복원은 백업을 뜬 노드에서 해야 했다. 이번에는 그렇게 하지 않았다. 컨트롤 플레인 노드의 호스트명은 ip-10-1-0-42.ec2.internal 같은 AWS 기본값이었고, 호스트명이 맞지 않아 첫 노드의 TLS 인증서를 다른 노드가 받아들이지 못했다. 팀은 새 인증서를 만들어 노드를 합류시켰다. 그 뒤 트래픽을 1% 에서 시작해 5%, 10%, 20%, 35%, 55%, 80%, 100% 로 조금씩 늘렸고, 손으로 멈춰 두었던 레거시 서비스를 조심스럽게 다시 켰다.\n\n원인은 장애가 끝나고 몇 시간 뒤에 찾았다. Calico 는 기본으로 모든 노드가 서로 피어링하지만, 큰 클러스터에서는 소수의 노드를 라우트 리플렉터로 두고 나머지 노드가 그 노드들과만 피어링하게 할 수 있다. 이 클러스터는 컨트롤 플레인 노드를 라우트 리플렉터로 쓰도록 설정되어 있었고, 그 nodeSelector 와 peerSelector 가 node-role.kubernetes.io/master 레이블을 가리켰다. 쿠버네티스는 1.20 대에서 master 라는 말을 control-plane 으로 바꿨고, 1.24 에서는 실행 중인 클러스터에서도 master 라는 이름을 지웠다. 이 설정은 몇 년 전 지금 Compute 팀의 전신이 만든 것이었다. 이 설정을 아는 사람은 모두 떠났고, Calico 전용 CLI 로 내려받아 손으로 고쳐 올리는 방식이라 어디에도 코드로 남아 있지 않았다. 사후 분석 중에 엔지니어 한 명이 이 기능을 쓰고 있었다는 것을 기억해 낸 덕분에 원인을 찾았다. 팀은 이것을 직접 원인으로 보고, 근본 원인은 클러스터마다 다른 맞춤형 구성이 쌓인 데 있다고 결론지었다."},"trigger":{"en":"An in-place upgrade of the legacy cluster from Kubernetes 1.23 to 1.24; in 1.24 the node-role.kubernetes.io/master label is removed even from running clusters.","ko":"Reddit 에서 가장 중요한 클러스터 가운데 하나인 레거시 클러스터를 쿠버네티스 1.23 에서 1.24 로 업그레이드했다."},"severity":"unknown","symptoms":["service-unavailable","high-error-rate","dns-resolution-failures","monitoring-down","pod-stuck-container-creating","control-plane-slow"],"timeline":{"detected_after":null,"resolved_after":"5h 14m"},"date_kind":"incident","detection":{"en":"The site broke about two minutes into the upgrade; an incident was opened immediately and responders were on the call within three minutes, working from CDN edge graphs because the cluster's own metrics were gone.","ko":"업그레이드를 시작하고 2분쯤 뒤 CDN 엣지의 상태 코드별 RPS 그래프에서 사이트가 멈춘 것이 보였다. 팀은 곧바로 장애를 선언했고 3분 안에 대응 인원이 통화에 모였다. 엣지 지표는 일부러 클러스터와 분리되어 있어 살아 있었지만, 클러스터 자체의 지표는 모두 사라졌다."},"components":["calico","kube-apiserver","admission-webhook","etcd"],"root_cause":"upgrade-breaking-change","remediation":[{"text":{"en":"deleted the Open Policy Agent webhook configurations to stop API server timeouts","ko":"API 서버에서 타임아웃을 내던 Open Policy Agent 의 웹훅 설정을 지웠다."}},{"text":{"en":"terminated all worker nodes of the cluster","ko":"복원 뒤 어차피 쓸 수 없게 될 워커 노드를 모두 종료했다."}},{"text":{"en":"restored the control plane from the pre-upgrade backup onto the first control plane node","ko":"컨트롤 플레인 노드 두 대를 종료하고, 남은 한 대의 구성 요소를 다운그레이드한 뒤 그 노드에 백업 데이터를 복원했다."}},{"text":{"en":"switched off the cluster autoscaler until the control plane was stable","ko":"단일 노드 컨트롤 플레인이 부하에 무너지지 않도록 클러스터 오토스케일러를 껐다."}},{"text":{"en":"generated new TLS certificates so replacement control plane nodes could join etcd","ko":"새 컨트롤 플레인 노드를 띄우고, 호스트명이 맞지 않던 TLS 인증서를 새로 만들어 노드들을 합류시켰다."}},{"text":{"en":"brought traffic back in steps from 1% to 100%","ko":"트래픽을 1% 에서 시작해 5%, 10%, 20%, 35%, 55%, 80%, 100% 로 조금씩 늘렸다."}},{"text":{"en":"restarted legacy services that had been stopped by hand","ko":"트래픽이 돌아올 때 문제를 일으키지 않도록 손으로 멈춰 두었던 민감한 레거시 서비스를 조심스럽게 다시 켰다."}}],"action_items":[{"text":{"en":"Codify cluster configuration so every choice has a record, and document in detail what cannot be codified","ko":"클러스터 구성을 코드로 관리해 모든 선택에 기록이 남게 하고, 코드로 만들 수 없는 것은 자세히 문서로 남긴다."}},{"text":{"en":"Standardise components across clusters and replace bespoke configurations","ko":"클러스터마다 다른 맞춤형 구성을 걷어 내고 모든 클러스터의 구성 요소를 표준화한다."}},{"text":{"en":"Build tooling to manage the lifecycle of whole clusters so they can be recreated or replicated","ko":"모든 클러스터를 최대한 같은 모습으로 유지하고 필요하면 다시 만들거나 복제할 수 있도록 클러스터 전체의 수명 주기를 관리하는 도구를 만든다."}},{"text":{"en":"Add pre-production clusters that can be tested with confidence","ko":"믿고 테스트에 쓸 수 있는 프리프로덕션 클러스터를 더 둔다."}},{"text":{"en":"Improve the backup restore process and make cluster upgrades safe","ko":"백업 복원 절차를 개선하고 클러스터 업그레이드를 안전하게 만든다."}}],"contributing":["config-not-in-code","staging-not-representative","backup-unverified","monitoring-blind-spot","single-point-of-failure"],"recurrence_of":null,"symptoms_text":{"en":"About two minutes after an engineer started the Kubernetes 1.23 to 1.24 upgrade of the cluster running Reddit's legacy stack, just after 19:00 UTC, the site stopped. The CDN edge request graph, which is kept separate from the cluster, showed traffic by status collapsing. Users saw the error page, a homepage with no posts, or plain errors.\n\nThe affected cluster had lost all its metrics, because Reddit's metrics are Kubernetes native, so responders had little to look at. DNS was failing in a strange way: records in Consul and in-cluster DNS names would not resolve, while public DNS records resolved normally.\n\nOver the next two hours more signs appeared. Pods took a very long time to start and stop, container images under 100MB took minutes to pull over a multi-gigabit link, and control plane logs were heavy without an obvious error. Calico pods were unhealthy: calico-kube-controllers was stuck in ContainerCreating, and deleted calico-typha pods were not recreated. API server logs showed many timeouts on write operations, which turned out to be timeouts calling the Open Policy Agent admission webhook. API server log volume had jumped fivefold at 19:04:49 UTC.\n\nThe responders deleted the stuck pod, restarted calico-typha, did a rolling restart of the control plane, then switched the entire control plane off and on, and finally deleted the OPA webhook configurations. The webhook timeouts stopped at once, but the cluster did not recover.","ko":"2023년 3월 14일 19시 UTC 직후 엔지니어가 Old Reddit 이라 불리는 레거시 스택을 돌리는 클러스터의 업그레이드를 시작했다. 2분쯤은 괜찮아 보였지만 곧 사이트 전체가 갑자기 멈췄다. CDN 엣지의 상태 코드별 RPS 그래프가 이를 보여 주었다. 사용자는 오류 페이지나 글이 하나도 없는 홈페이지를 보았다.\n\n영향을 받은 클러스터는 지표를 모두 잃었다. 지표가 쿠버네티스 안에서 수집되고 있었기 때문이다. 유일하게 눈에 띈 것은 DNS 였다. Consul 에 있는 레코드와 클러스터 내부 DNS 레코드는 조회되지 않았지만, 이상하게도 공개 DNS 레코드는 잘 조회됐다. 이전 업그레이드나 운영이 아닌 환경의 테스트에서는 본 적 없는 문제였다.\n\n팀은 로그를 뒤지며 몇 가지를 발견했다. 파드가 시작하고 멈추는 데 아주 오래 걸렸다. 100MB 도 안 되는 컨테이너 이미지를 수 기가비트 연결로 받는 데 몇 분씩 걸렸다. 컨트롤 플레인 로그는 많이 쏟아졌지만 뚜렷한 오류는 없었다. Calico 파드도 정상이 아니었고, calico-kube-controllers 파드는 ContainerCreating 상태에 멈춰 있었다.\n\n팀은 calico-kube-controllers 파드를 지웠지만 소용없었다. calico-typha 파드를 지웠더니 새 파드가 몇 분이 지나도 만들어지지 않았다. 컨트롤 플레인 구성 요소를 롤링 재시작했고, 컨트롤 플레인 전체를 껐다가 다시 켜기도 했지만 달라진 것은 없었다. API 서버 로그에는 쓰기 작업 자체가 아니라 admission controller 웹훅을 호출하다 난 타임아웃이 많았다. 모든 리소스를 감시하는 웹훅은 Open Policy Agent 뿐이었고, 어차피 내려가 있었으므로 그 웹훅 설정을 지웠다. 타임아웃은 곧바로 사라졌지만 클러스터는 회복되지 않았다.\n\n나중에 로그를 보니 19시 4분 49초 UTC 에 API 서버 로그가 폭증해 양이 다섯 배로 늘었다. 그보다 약 5초 전에 OPA 로그가 완전히 끊겼다. 혼란이 시작되기 2초 전에는 클러스터 전체의 calico-node 가 먼저 업그레이드한 첫 번째 컨트롤 플레인 노드로 가는 경로를 지우기 시작했고, 곧이어 모든 노드로 가는 경로를 지웠다."},"updated_at":"2026-11-10T00:00:39.054599Z","url":"https://benchlet.com/incidentlib/app/cases/IL-0060","org_slug":"reddit"}