{"id":"IL-0192","org":"Grafana Labs","date":"2025-02-18","docs":[],"title":{"en":"A Crossplane composition merge-policy change for a TLS policy update switched Kubernetes Services from LoadBalancer to ClusterIP and deleted the load balancers behind about 25% of Grafana Cloud services","ko":"Crossplane 컴포지션의 병합 정책 변경이 Service 타입을 ClusterIP 로 바꿔 클라우드 로드 밸런서가 지워지고 Grafana Cloud 서비스의 약 25%가 끊겼다."},"domain":["kubernetes","networking"],"impact":{"scope":"About 25% of Grafana Cloud services, including endpoints for metrics and logs ingestion","users":"Customers of the affected Grafana Cloud services; customers whose agents could not buffer may have lost metrics or logs","region":"unknown"},"review":{"written_by":"session","reviewed_at":null,"reviewed_by":null},"lessons":{"en":"A change that looked small and low-risk had a side effect that only a full end-to-end test through Crossplane and the cloud provider's load balancer controller would have shown; seeing the intended override work was not that test. Staged rollout and blast-radius limits that exist only as process can be skipped, so Grafana Labs moved to enforce waves in CI and to add deletion protection on the load balancers themselves, beneath the Crossplane layer.","ko":"작은 변경이라 위험이 낮다고 가정하고 충분한 시험과 영향 범위 줄이기를 건너뛴 것이 장애로 이어졌다. 웨이브 배포는 절차로 기대하는 데 그치지 않고 시스템이 강제해야 한다. 비상 예외는 웨이브를 고르지 않았을 때의 기본 동작이 아니라 말 그대로 예외여야 한다. 이번 변경은 시험을 안 한 것이 아니라 시험한 범위가 충분하지 않았다. 클라우드 로드 밸런서 컨트롤러와 자체 도구까지 처음부터 끝까지 돌려 봐야 의도한 작은 변화만 일어나는지 알 수 있었다. 개발 웨이브부터만 적용하도록 강제했다면 제대로 시험할 수밖에 없었을 것이다."},"related":[],"sources":[{"url":"https://grafana.com/blog/2025/03/13/how-we-responded-to-a-2-hour-partial-outage-in-grafana-cloud","kind":"blog","license":"unknown","archived":null}],"summary":{"en":"An engineer was upgrading the TLS policy version on the cloud load balancers used by many Grafana Cloud services, including metrics and logs ingestion. The engineer tested the new configuration by hand in the development environment and confirmed it could be applied without redeploying the load balancers. Grafana Labs manages many cloud provider resources from Kubernetes with Crossplane, so the code change was made in a Crossplane composition: its merge policy was changed so that the default TLS policy could be overridden. The engineer saw the override survive reconciliation and took that as enough of a test, so the full redeployment and test in development was skipped. The pull request was reviewed by a peer before it was merged.\n\nThe new merge policy also overrode a default value from the base configuration that was never meant to change. Many Kubernetes Services switched from type LoadBalancer to type ClusterIP, and Kubernetes destroyed their load balancers. About 25% of Grafana Cloud services were affected for about 150 minutes. Grafana Labs normally rolls changes out in waves, first development, then staging, then canary and production in steps, observing each wave, typically for at least 24 hours, with what is effectively a feature flag deciding which wave gets a change. This change had no such flag and was applied manually, and nothing stopped it from reaching development, staging and production at once. Deletion protection at the Crossplane level did not help, because the Crossplane resources were not deleted; they were changed so that the objects underneath became invalid.\n\nThe team rolled back the code change and recreated the affected Kubernetes Services so that Kubernetes created the load balancers again. Most applications were fully recovered by 11:10 UTC. It was not a security incident, and no customer data was leaked or exposed.","ko":"한 엔지니어가 메트릭과 로그 수집을 포함한 여러 서비스가 쓰는 로드 밸런서의 TLS 정책 버전을 올리고 있었다. 개발 환경에서 설정을 바꾸고 손으로 시험해, 로드 밸런서를 다시 배포하지 않고도 바꿀 수 있다는 것을 확인했다. Grafana Labs 는 클라우드 리소스 상당수를 쿠버네티스 위의 Crossplane 으로 관리한다. 엔지니어는 기본 TLS 정책을 덮어쓸 수 있도록 Crossplane 컴포지션의 병합 정책을 바꿨고, Crossplane 이 설정을 다시 맞춘 뒤에도 덮어쓴 값이 남아 있는 것을 보고 충분히 시험했다고 판단했다. 원래는 개발 환경에서 전체 재배포와 시험을 했어야 했다.\n\n이 변경은 기본 설정에 있던 기본값 하나까지 의도치 않게 덮어썼다. 그 결과 쿠버네티스 Service 여러 개의 타입이 LoadBalancer 에서 ClusterIP 로 바뀌었고 쿠버네티스가 그 로드 밸런서를 지웠다. Grafana Labs 는 평소 변경을 웨이브라고 부르는 단계로 나눠 개발, 스테이징, 운영 카나리, 운영 전체 순으로 배포하고, 웨이브마다 보통 24시간 이상 지켜본다. 이번 변경도 PR 리뷰를 거쳤지만, 웨이브를 고르는 플래그 없이 나가면 웨이브 없이 배포되는 구조였다. 개발 환경 적용도 자동화 절차가 아니라 손으로 했기 때문에 Crossplane 오퍼레이터가 Service 를 바꾸는 동작을 미리 보지 못했다. 시스템은 개발, 스테이징, 운영 환경을 한꺼번에 바꾸는 것을 막지 않았다.\n\n팀은 코드 변경을 롤백하고 영향받은 Service 를 다시 만들어 쿠버네티스가 로드 밸런서를 새로 만들게 했고, 11:10 UTC 에 대부분의 애플리케이션을 복구했다. 장애는 약 150분 이어졌다. 보안 사고는 아니었고 고객 데이터가 새거나 노출되지 않았다."},"trigger":{"en":"A change to the merge policy of a Crossplane composition, made so that the default TLS policy of the load balancers could be overridden, was applied to development, staging and production at the same time.","ko":"로드 밸런서의 TLS 정책 버전을 올리면서, 기본 TLS 정책을 덮어쓸 수 있도록 Crossplane 컴포지션의 병합 정책을 바꾼 변경이 개발, 스테이징, 운영 환경에 동시에 적용됐다."},"severity":"unknown","symptoms":["service-unavailable","resources-deleted"],"timeline":{"detected_after":null,"resolved_after":"2h 30m"},"date_kind":"incident","detection":{"en":"Application teams noticed at 08:41 UTC that some of their endpoints were no longer available.","ko":"08:41 UTC 에 애플리케이션 팀 여러 곳이 자기 엔드포인트 일부에 접속이 되지 않는 것을 알아챘다. 조사해 보니 최근의 TLS 정책 설정 변경 때문에 그 엔드포인트의 로드 밸런서가 사라져 있었다."},"components":["config-templating","cloud-load-balancer"],"root_cause":"config-change","remediation":[{"text":{"en":"rolled back the Crossplane composition change","ko":"문제의 코드 변경을 롤백했다."}},{"text":{"en":"recreated the affected Kubernetes Services so that Kubernetes recreated their load balancers","ko":"영향받은 쿠버네티스 Service 를 다시 만들어 쿠버네티스가 로드 밸런서를 새로 만들게 했다."}}],"action_items":[{"text":{"en":"Add a CI check that blocks a deployment to more than one wave, so any exception to the dev, staging, canary, production order is caught","ko":"한 번에 두 개 이상의 웨이브로 배포하는 것을 막는 CI 검사를 넣어, 개발에서 스테이징, 카나리, 운영으로 이어지는 순서를 벗어나는 배포를 잡아내게 했다."}},{"text":{"en":"Give higher priority to a more sophisticated wave design that was already under discussion","ko":"논의만 하던 더 정교한 웨이브 설계의 우선순위를 올려 개선안을 합의하고 구현한다."}},{"text":{"en":"Enable deletion protection on the load balancers, one layer below Crossplane","ko":"실수로 지워지지 않도록 로드 밸런서에 삭제 보호를 켜기 시작했다. Crossplane 수준의 삭제 보호보다 한 층 아래에서 막는다."}},{"text":{"en":"Enable deletion protection on other critical cloud provider resources","ko":"로드 밸런서 말고도 중요한 클라우드 리소스를 하나씩 살펴 필요한 곳에 삭제 보호를 켠다."}},{"text":{"en":"Develop the Crossplane Managed Resources exporter for change validation and visibility, now in beta testing within Platform engineering; simulated against the change that caused this outage, it shows a clear diff","ko":"변경 검증과 가시성을 높이려고 만든 Crossplane Managed Resources exporter 를 플랫폼 엔지니어링 안에서 베타로 시험하고 있다. 이번 장애를 일으킨 변경에 적용해 보면 차이가 분명히 드러난다."}},{"text":{"en":"Complete and test support for reconfiguring the TLS policy so application teams can deploy it","ko":"TLS 정책을 다시 설정하는 기능을 제대로 완성하고 시험해, 애플리케이션 팀이 배포할 수 있게 했다."}}],"contributing":["assumption-unverified","no-staged-rollout","blast-radius-too-wide","no-safeguard-on-destructive-change"],"recurrence_of":null,"symptoms_text":{"en":"At 08:41 UTC on 18 February 2025, several Grafana Labs application teams noticed that some of their endpoints were no longer available. The investigation quickly found that the load balancers for those endpoints were gone.\n\nThe load balancers served many services, including the ingestion of metrics and logs. Customers lost access to Grafana Cloud services, and customers whose agents could not buffer for the length of the outage may have lost metrics or logs before they reached Grafana Cloud.\n\nThe missing load balancers traced back to a recent configuration change made while updating the TLS policies on the load balancers. The Crossplane resources still existed; what had changed were the Kubernetes Services underneath, many of which were now of type ClusterIP instead of LoadBalancer.","ko":"2025년 2월 18일 08:41 UTC, Grafana Labs 의 애플리케이션 팀 여러 곳이 자기 엔드포인트 일부에 접속이 되지 않는 것을 알아챘다.\n\n조사해 보니 그 엔드포인트 앞에 있던 로드 밸런서가 사라져 있었다. 쿠버네티스 Service 여러 개의 타입이 LoadBalancer 에서 ClusterIP 로 바뀌어 있었고, 쿠버네티스가 그에 딸린 클라우드 로드 밸런서를 지운 것이었다. 메트릭과 로그 수집 엔드포인트를 포함해 Grafana Cloud 서비스의 약 25%가 영향을 받았다.\n\nCrossplane 리소스는 지워지지 않고 그대로 남아 있었다. 그래서 Crossplane 수준에 걸어 둔 삭제 보호도 막지 못했다. 리소스는 남은 채 그 아래의 객체만 잘못된 상태로 바뀌었다.\n\n장애 동안 데이터를 버퍼에 쌓아 두지 못하는 에이전트를 쓰던 고객은 수집되기 전의 메트릭이나 로그 일부를 잃었을 수 있다."},"updated_at":"2026-11-10T00:00:40.192099Z","url":"https://benchlet.com/incidentlib/app/cases/IL-0192","org_slug":"grafana-labs"}