{"id":"IL-0061","org":"Spotify","date":"2019-05-22","docs":[],"title":{"en":"A Terraform pull request merged out of order deleted a production GKE cluster, and recreating it deleted a second one","ko":"Terraform 상태 파일이 리뷰 빌드에서 바뀐 데다 권한 차이까지 겹쳐 Terraform 이 운영 GKE 클러스터 세 개 중 두 개를 삭제했다"},"domain":["kubernetes","gcp"],"impact":{"scope":"two of three production GKE clusters deleted; no reported impact on end users","users":"no reports of end-user impact; internal teams had to add VMs, update hardcoded master IPs and refresh kubectl credentials","region":"Asia and one other region"},"review":{"written_by":"session","reviewed_at":null,"reviewed_by":null},"lessons":{"en":"Plan for failure from the start and migrate large infrastructure gradually, so there is redundancy and a way back. A backup whose restore has never been tested is not a backup.\n\nPractising disaster recovery paid off: restoring the first deleted cluster took three and a quarter hours, and the team can now restore a larger cluster in one hour. What matters is not who made the mistake but what was learned from it.","ko":"Terraform 은 상태 파일을 보고 할 일을 정한다. 리뷰 빌드가 운영 상태 파일을 바꾸게 두면 머지 순서 하나로 운영 리소스가 지워질 수 있다. 같은 리소스도 API 를 호출하는 권한에 따라 다른 속성으로 보이므로, 가져올 때와 관리할 때 권한이 다르면 Terraform 이 그 차이를 변경으로 읽는다. 처음부터 실패에 대비하고, 큰 인프라를 서서히 옮기고, 되돌릴 방법을 남겨 둔 덕분에 운영 클러스터 두 개가 사라져도 최종 사용자 영향이 없었다. 복원을 시험해 보지 않은 백업은 백업이 아니다. 실수를 누가 했는지보다 무엇을 배웠고 같은 종류의 실수를 어떻게 막을지가 중요하다."},"related":[],"sources":[{"url":"https://www.youtube.com/watch?v=ix0Tw8uinWs","kind":"talk","license":"unknown","archived":null}],"summary":{"en":"At the time Spotify ran three production GKE clusters, one each in the US, Europe and Asia, backed up every hour. Stateless backend services were moving to GKE only partially: teams were told to put part of each service's capacity on Kubernetes and keep the rest on ordinary VMs. After an engineer deleted a 50-node production cluster in November 2018 by working in the wrong browser tab, the team started managing clusters with Terraform, from a git repository that both the cluster operators and the feature teams commit to.\n\nThe team did not yet understand that Terraform decides what to do from its state file. Their pull request to import the Asia cluster ran review builds that modified the global state file used for production, before the pull request was merged. A few minutes later a feature team opened an innocuous-looking pull request adding three users to an RBAC binding; what mattered was what it lacked, the definition of the Asia cluster. That pull request was merged first, Terraform found a cluster in state that was not in the configuration, and deleted it.\n\nMerging the import pull request to recreate the cluster failed halfway: Terraform now ran as a service account that lacked permissions the engineers' personal owner accounts had had. The service account was given the permissions it needed, but these differed from those used for the import, and with different permissions the Google APIs return different attributes for a cluster. Terraform saw the clusters as changed and deleted another production cluster. Recovery from this second deletion lasted from 8 in the evening until 5 in the morning.\n\nListeners were not affected because of choices made for the migration. Services ran only partly on Kubernetes, and service discovery registered pod IPs directly. After the operators removed a deleted cluster from the service discovery configuration and restarted it, the pod IPs dropped out and clients moved to the instances outside Kubernetes. The hourly cluster backups, whose restore had already been tested, were essential to the recovery.","ko":"Spotify 는 상태 없는 백엔드 서비스를 GKE 로 옮기는 중이었다. 미국, 유럽, 아시아에 운영 클러스터를 하나씩 두고 매시간 백업했다. 2018년 11월 한 엔지니어가 브라우저 탭을 잘못 골라 50노드 운영 클러스터를 지운 일이 있었다. 팀은 같은 실수를 막으려고 Terraform 으로 클러스터를 코드로 관리하기 시작했다. 이 사례는 그 도입 과정에서 일어났다.\n\n클러스터 설정은 클러스터 사용자와 운영 팀이 함께 쓰는 git 저장소에 있었다. 팀은 Terraform 이 상태 파일을 보고 할 일을 정한다는 점을 잘 알지 못했다. 아시아 클러스터를 상태 파일로 가져오는 PR 의 리뷰 빌드가 운영에 영향을 주는 전역 상태 파일을 바꿨다. 이 PR 은 아직 머지되지 않은 상태였다. 몇 분 뒤 Terraform 을 모르는 클러스터 사용자가 기존 롤 바인딩 파일에 사용자 세 명을 추가하는 PR 을 올렸다. 변경은 무해해 보였지만 그 PR 에는 아시아 클러스터 정의가 없었다. 두 PR 은 순서가 뒤바뀌어 머지됐고, Terraform 은 정의에 없는 아시아 클러스터를 지웠다.\n\n팀은 아시아 클러스터를 정의한 PR 을 머지해 클러스터를 다시 만들려 했다. 전에는 각자 노트북에서 프로젝트 소유자 권한이 있는 개인 GCP 계정으로 Terraform 명령을 돌렸지만, 이제는 서비스 계정으로 돌렸다. 이 계정에 권한이 모자라 생성 스크립트가 중간에 실패했다. 팀이 서비스 계정에 권한을 더 주자 Terraform 이 받는 클러스터 속성이 처음 가져올 때와 달라졌고, Terraform 은 다른 운영 클러스터 하나를 더 지웠다. 이 두 번째 장애는 밤 8시부터 새벽 5시까지 이어졌다.\n\n최종 사용자 영향은 보고되지 않았다. 팀은 쿠버네티스 사용을 베타로 두고 각 서비스가 용량 일부만 쿠버네티스로 옮기도록 권했다. 사내 서비스 디스커버리는 쿠버네티스 서비스가 아니라 파드 IP 를 등록했다. 클러스터가 사라지자 트래픽은 계획대로 쿠버네티스 밖 인스턴스로 넘어갔다. 내부에서는 한 팀이 VM 을 더 만들었다. 운영 팀은 옛 마스터 IP 를 하드코딩한 곳을 모두 고쳤다. kubectl 사용자는 자격 증명을 새로 받아야 했다."},"trigger":{"en":"A feature team's pull request that added three users to an existing RBAC role binding file was merged before the cluster operators' pull request that imported the Asia cluster into Terraform state. The review build of the import pull request had already written the cluster into the shared state file, and the merged configuration had no definition of the Asia cluster, so Terraform removed it.","ko":"클러스터 운영 팀이 아시아 클러스터를 Terraform 상태 파일로 가져오는 PR 을 올렸고, 그 리뷰 빌드가 운영에 쓰이는 전역 상태 파일을 바꿨다. 그런데 이 PR 보다 클러스터 사용자의 PR 이 먼저 머지됐고, 그 PR 에는 아시아 클러스터 정의가 없었다. Terraform 은 정의에 없는 아시아 클러스터를 삭제했다."},"severity":"unknown","symptoms":["resources-deleted"],"timeline":{"detected_after":null,"resolved_after":"9h"},"date_kind":"published","detection":{"en":"Internal service discovery could not reach the deleted cluster and the cluster operator team was paged.","ko":"사내 서비스 디스커버리가 사라진 클러스터에 닿지 못하자 클러스터 운영 팀이 호출을 받았다."},"components":["terraform","ci-system","gke-control-plane"],"root_cause":"config-change","remediation":[{"text":{"en":"removed the deleted clusters from the service discovery configuration and restarted service discovery so clients fell back to the instances outside Kubernetes","ko":"서비스 디스커버리 설정에서 사라진 클러스터를 빼고 사내 서비스 디스커버리를 재시작해 파드 IP 를 백엔드 목록에서 지웠고, 트래픽을 쿠버네티스 밖 인스턴스로 넘겼다."}},{"text":{"en":"one team that ran half on Kubernetes and half on VMs created more VMs outside Kubernetes to handle the load","ko":"쿠버네티스와 VM 에 반씩 나눠 돌던 한 팀이 부하를 받으려고 쿠버네티스 밖 VM 을 더 만들었다."}},{"text":{"en":"recreated the clusters, using the hourly backups","ko":"미리 복원을 시험해 둔 클러스터 백업으로 삭제된 클러스터를 다시 만들었다."}},{"text":{"en":"updated the places where the old clusters' master IPs were hardcoded","ko":"옛 클러스터 마스터 IP 를 하드코딩한 곳을 찾아 모두 새 값으로 고쳤다."}}],"action_items":[{"text":{"en":"Post the Terraform dry-run output as a comment on each pull request","ko":"드라이런 결과를 PR 댓글로 달아 무엇이 바뀔지 바로 보이게 했다."}},{"text":{"en":"Require a status check that the branch is up to date with master, so the dry run reflects what will actually happen","ko":"머지 전에 브랜치가 최신 master 를 반영해야 하는 필수 상태 검사를 켜서, 드라이런이 실제로 일어날 일을 보여 주게 했다."}},{"text":{"en":"Require an approving review on pull requests","ko":"PR 머지에 승인 리뷰를 필수로 했다."}},{"text":{"en":"Fail reviews automatically when certain keywords such as destroy appear; cluster deletions are done manually outside this repository","ko":"destroy 같은 키워드가 보이면 리뷰를 바로 실패시키고, 클러스터 삭제는 이 저장소 밖에서 손으로 하게 했다."}},{"text":{"en":"Add linters and validators for the cluster configuration","ko":"클러스터 설정을 린터와 검증기로 검사하게 했다."}},{"text":{"en":"Schedule and announce disaster recovery tests that cover a cluster going away and smaller failures","ko":"일정을 미리 잡아 널리 알린 뒤, 클러스터가 사라지는 경우와 그보다 작은 실패를 다루는 재해 복구 훈련을 했다."}}],"contributing":["no-safeguard-on-destructive-change","review-missed","assumption-unverified"],"recurrence_of":null,"symptoms_text":{"en":"Terraform, applied from the shared cluster configuration repository after a merge, deleted Spotify's production GKE cluster in Asia. While the team tried to recreate it, Terraform deleted a second production cluster, so two of the three production clusters were gone.\n\nThe internal service discovery system polls pod IPs from each cluster; when it could not reach a cluster, the cluster operator team was paged. Inside the company, one team had to create more VMs outside Kubernetes to carry the load, the operators had to find and update every place where the old clusters' master IPs were hardcoded, and everyone using kubectl had to refresh cluster credentials because the recreated clusters had new certificates. Otherwise the internal user support Slack channel stayed quiet, and there were no reports of impact on listeners.","ko":"Terraform 이 아시아 운영 GKE 클러스터를 지웠다. 사내 서비스 디스커버리가 사라진 클러스터에 닿지 못하자 클러스터 운영 팀이 호출을 받았다.\n\n아시아 클러스터를 정의한 PR 은 아직 머지되지 않은 상태였다. 팀은 그 PR 을 머지하면 클러스터가 다시 생길 것이라고 봤다. 그런데 클러스터 생성 스크립트가 권한이 모자라 중간에 실패했다.\n\n팀이 서비스 계정에 필요한 권한을 주고 다시 돌리자 Terraform 은 클러스터를 처음 가져올 때와 다른 권한으로 GCP API 를 호출했다. 권한에 따라 API 가 보여 주는 클러스터 속성이 달라서 Terraform 에는 클러스터가 바뀐 것으로 보였다. Terraform 은 운영 클러스터 하나를 더 지웠고, 운영 클러스터 세 개 중 두 개가 사라졌다.\n\n사용자 지원 슬랙 채널은 조용했고 사용자 쪽에서 들어온 호출도 없었다. 한 팀은 부하를 받기 위해 쿠버네티스 밖 VM 을 더 만들어야 했다. kubectl 사용자는 다시 만든 클러스터의 새 인증서 때문에 자격 증명을 새로 받아야 했다."},"updated_at":"2026-11-10T00:00:39.107663Z","url":"https://benchlet.com/incidentlib/app/cases/IL-0061","org_slug":"spotify"}