구독해서 새 게시물에 대한 알림을 받으세요.

한 줄 쿠버네티스 수정으로 연간 600시간 절약

2026-03-26

4분 읽기
이 게시물은 English日本語로도 이용할 수 있습니다.

본 콘텐츠는 사용자의 편의를 고려해 자동 기계 번역 서비스를 사용하였습니다. 영어 원문과 다른 오류, 누락 또는 해석상의 미묘한 차이가 포함될 수 있습니다. 필요하시다면 영어 원문을 참조하시기를 바랍니다.

저희가 Terraform 변경 사항을 계획하고 적용하는 데 사용하는 도구인 Atlantis를 재시작할 때마다, 변경 사항이 다시 적용될 때까지 30분 동안 멈춰 서야 했습니다. Atlantis로 관리하는 리포지토리는 계획, 적용, 인프라 변경이 불가능합니다. 한 달에 약 100건 가량이 자격 증명 로테이션 및 온보딩을 위해 재시작되었으며, 이는 매달 최대 50시간 이상의 엔지니어링 시간이 차단되고 매번 당직 엔지니어를 호출하는 시간이 되었습니다.

이는 궁극적으로 쿠버네티스의 안전 기본값으로 인해 발생했으며, Atlantis에서 사용하는 영구 볼륨이 수백만 개의 파일로 증가함에 따라 조용히 병목 현상을 일으켰습니다. 다음은 Cloudflare가 이를 추적해 한 줄 변경으로 해결한 방법입니다.

재시작이 이상할 정도로 느림

저희는 계획 및 적용을 처리하는 Atlantis를 사용하여 GitLab 병합 요청(MR)으로 수십 개의 Terraform 프로젝트를 관리하고 있습니다. MCP는 잠금을 적용하여 한 번에 하나의 MR만 프로젝트를 수정할 수 있도록 합니다. 

이는 싱글톤 StatefulSet으로 쿠버네티스에서 실행되며 디스크의 리포지토리 상태를 추적하기 위해 쿠버네티스 영구 볼륨(PV)에 의존합니다. Terraform 프로젝트를 온보딩 또는 오프보딩해야 하거나 Terraform에서 사용하는 자격 증명을 업데이트할 때마다 변경 사항을 적용하려면 우리는 Atlantis를 다시 시작해야 하며, 이 프로세스는 30분이 걸릴 수 있습니다.

재시작 속도의 느린 재시작은 최근 Atlantis에서 사용하는 영구 스토리지의 inone이 부족하여 볼륨 크기를 조정하기 위해 Atlantis를 재시작해야 했습니다. 아이노드는 디스크의 각 파일 및 디렉터리 항목에서 사용하며 파일 시스템에서 사용할 수 있는 노드 수는 파일 시스템을 만들 때 전달된 매개변수에 따라 결정됩니다. Cloudflare의 쿠버네티스 플랫폼에서 제공되는 Ceph 영구 스토리지 구현은 mkfs에 플래그를 전달하는 방법을 제공하지 않으므로 기본값에 종속되어 있습니다. 포드가 다시 시작됩니다. 

경고 기간을 연장하는 것에 대해 이야기했지만, 그렇게 하면 문제가 가려지고 실제 문제에 대한 대응이 지연될 뿐입니다. 우리는 대신 작업이 그렇게 오래 걸리는 이유를 정확히 조사하기로 결정했습니다.

잘못된 행동

Atlantis가 사용하는 비밀에 대한 변경 사항을 적용하기 위해 Atlantis를 롤링 재시작하라는 요청을 받으면, 기존 Atlantis 포드를 정상적으로 종료한 후 새 포드를 가동하는 kubectl rollout restart statefulset atlantis를 실행할 것입니다. 새 포드가 거의 즉시 나타나지만, 내용을 살펴보면 다음과 같습니다.

$ kubectl get pod atlantis-0
atlantis-0                                                        0/1     
Init:0/1     0             30m

...그래서 무엇을 제공할까요? 당연히 가장 먼저 확인해야 할 것은 해당 포드에 대한 이벤트입니다. 주변에서 init 컨테이너가 실행되기를 기다리고 있는데, 포드 이벤트가 그 이유를 밝혀낼 수 있습니다.

$ kubectl events --for=pod/atlantis-0
LAST SEEN   TYPE      REASON                   OBJECT                   MESSAGE
30m         Normal    Killing                  Pod/atlantis-0   Stopping container atlantis-server
30m        Normal    Scheduled                Pod/atlantis-0   Successfully assigned atlantis/atlantis-0 to 36com1167.cfops.net
22s         Normal    Pulling                  Pod/atlantis-0   Pulling image "oci.example.com/git-sync/master:v4.1.0"
22s         Normal    Pulled                   Pod/atlantis-0   Successfully pulled image "oci.example.com/git-sync/master:v4.1.0" in 632ms (632ms including waiting). Image size: 58518579 bytes.

이는 거의 정상적이지만... 포드 일정을 예약하는 시점과 실제로 init 컨테이너의 이미지를 가져오기 시작하는 시점 사이에 이렇게 긴 시간이 걸리는 이유는 무엇일까요? 안타깝게도 쿠버네티스 자체에 있는 데이터가 이것의 전부였습니다. 하지만 실제로 포드를 실행하기 시작하는 데 왜 그렇게 오랜 시간이 걸리는지 알려줄 수 있는 무언가가 더 있어야 할 것이 분명했습니다.

자세히 알아보기

쿠버네티스의 각 노드에서 실행되는 kubelet 이라는 구성 요소는 포드 생성 조정, 영구 볼륨 탑재 등의 작업을 담당합니다. 저는 Kubernetes 팀에서 근무할 때부터 kubelet 이 systemd 서비스로 실행되므로 Kibana에서 로그를 확인할 수 있다는 것을 알고 있습니다. 포드가 예약되었으므로 관심이 있는 호스트 이름을 알고 있으며 kubelet 의 로그 메시지에 연결된 개체가 포함되어 있으므로 atlantis 를 필터링하여 로그 메시지에서 흥미로운 내용을 찾을 수 있습니다.

우리는 포드 일정이 끝난 후 장착되는 Atlantis 태양광 발전을 관찰할 수 있었습니다. 또한 모든 비밀 볼륨이 문제 없이 탑재되는 것이 관찰되었습니다. 하지만 로그에는 여전히 설명할 수 없는 큰 격차가 있었습니다. 다음과 같은 결과를 확인했습니다.

[operation_generator.go:664] "MountVolume.MountDevice succeeded for volume \"pvc-94b75052-8d70-4c67-993a-9238613f3b99\" (UniqueName: \"kubernetes.io/csi/rook-ceph-nvme.rbd.csi.ceph.com^0001-000e-rook-ceph-nvme-0000000000000002-a6163184-670f-422b-a135-a1246dba4695\") pod \"atlantis-0\" (UID: \"83089f13-2d9b-46ed-a4d3-cba885f9f48a\") device mount path \"/state/var/lib/kubelet/plugins/kubernetes.io/csi/rook-ceph-nvme.rbd.csi.ceph.com/d42dcb508f87fa241a49c4f589c03d80de2f720a87e36932aedc4c07840e2dfc/globalmount\"" pod="atlantis/atlantis-0"
[pod_workers.go:1298] "Error syncing pod, skipping" err="unmounted volumes=[atlantis-storage], unattached volumes=[], failed to process volumes=[]: context deadline exceeded" pod="atlantis/atlantis-0" podUID="83089f13-2d9b-46ed-a4d3-cba885f9f48a"
[util.go:30] "No sandbox for pod can be found. Need to start a new one" pod="atlantis/atlantis-0"

마지막 메시지 두 개가 실제로 제대로 시작되는 것을 관찰할 때까지 여러 번 반복되었습니다.

따라서 kubelet 은 포드가 준비되었다고 생각하지만, 시작되지 않고 무언가가 시간 초과됩니다.

누락된 조각

작업 모음에서 가지고 있었던 가장 낮은 수준의 로그로는 어떤 일이 일어나고 있는지 알려주지 않았습니다. 또 무엇을 살펴봐야 할까요? 마지막으로 메시지가 중단되기 전, 이 메시지는PV가 노드에 장착된다는 것입니다. 일반적으로 태양광 발전에 실장에 문제가 있는 경우(예: 다른 노드에 탑재되어 있지 않기 때문에) 이는 이벤트로 버블링됩니다. 하지만 여전히 문제는 발생하며, 드릴 다운해야 할 유일한 항목은PV 자체입니다. pv 이름은 좋은 검색어가 될 만큼 고유하므로 이를 Kibana에 연결했습니다...

[volume_linux.go:49] Setting volume ownership for /state/var/lib/kubelet/pods/83089f13-2d9b-46ed-a4d3-cba885f9f48a/volumes/kubernetes.io~csi/pvc-94b75052-8d70-4c67-993a-9238613f3b99/mount and fsGroup set. If the volume has a lot of files then setting volume ownership could be slow, see https://github.com/kubernetes/kubernetes/issues/69699

서두에서 inod가 부족할 것이라고 말했던 것 기억하시나요? 다시 말해, 이 PV에 많은 파일이 있습니다. PV가 탑재되면 kubeletchgrp -R 을 실행하여 이 파일 시스템에 있는 모든 파일과 폴더의 그룹을 재귀적으로 변경합니다. 빠른 플래시 스토리지를 사용하더라도 항목의 양이 엄청납니다.

포드의 spec.securityContext 포함된 fsGroup: 1은 GID 1에서 실행 중인 프로세스가 볼륨의 파일에 액세스할 수 있도록 보장합니다. Atlantis는 루트가 아닌 사용자로 실행되므로 이 설정이 없으면PV에 읽거나 쓸 권한이 없습니다. 쿠버네티스가 이를 적용하는 방식은 전체 PV의 소유권을 탑재될 때마다 재귀적으로 업데이트하는 것입니다.

해결 방법

이 문제를 해결하는 것은 정말... 지루했습니다. 버전 1.20부터 Kubernetes는 pod.spec.securityContext 에 추가 필드를 지원했습니다 fsGroupChangePolicy라고 불립니다. 이 필드는 기본적으로 항상으로 설정되어 있어 여기 보이는 그대로의 동작으로 이어집니다. 또 다른 옵션인 OnRootMismatch가 있어, PV의 루트 디렉터리에 올바른 권한이 없는 경우에만 권한을 변경할 수 있습니다. 파일이 PV에 어떻게 생성되는지 정확히 모르는 경우 fsGroupChangePolicy: OnRootMismatch를 설정하지 마십시오. 우리는PV의 어떤 항목에서도 그룹을 변경하는 것이 없어야 함을 확인한 다음, 해당 필드를 설정했습니다.

spec:
  template:
    spec:
      securityContext:
        fsGroupChangePolicy: OnRootMismatch

이제 Atlantis를 다시 시작하는 데 걸리는 시간은 약 30초로, Atlantis를 다시 시작하는 데 30분 걸렸던 것입니다.

기본 Kubernetes 설정은 볼륨이 적은 경우에는 합리적이지만, 데이터가 증가함에 따라 병목 현상이 발생할 수 있습니다. 저희의 경우, fsGroupChangePolicy에 대한 이 한 줄의 변경으로 매달 거의 50시간에 달하는 중단된 엔지니어링 시간을 확보할 수 있었습니다. 이는 팀이 인프라 변경이 완료될 때까지 기다리는 시간이고, 긴급 엔지니어가 잘못된 경보에 대응해야 하는 시간이었습니다. 이는 배포보다 진단하는 데 시간이 더 오래 걸렸던 수정 프로그램에서 연간 600시간 가량이 생산적인 업무에 복귀한 셈입니다.

쿠버네티스의 안전한 기본값은 작고 단순한 워크로드를 위해 설계되었습니다. 하지만 확장함에 따라 서서히 병목 현상이 생길 수 있습니다. 영구 볼륨이 큰 워크로드를 실행하는 경우, 이와 같은 재귀적 권한 변경이 자동으로 재시작 시간을 방해하는지 확인하는 것이 좋습니다. securityContext 설정(특히 fsGroupfsGroupChangePolicy)을 감사하세요. OnRootMismatch는 v1.20부터 사용할 수 있습니다.

모든 수정이 강력하거나 복잡한 것은 아니며, 일반적으로 "왜 시스템이 이런 식으로 작동하는가?"라는 질문을 던질 가치가 있습니다.

인프라 문제를 대규모로 디버깅하는 것이 흥미롭다면, 채용 중입니다. Cloudflare Community 또는 Discord에 참여하여 업무에 대해 이야기해 보세요.

Cloudflare에서는 전체 기업 네트워크를 보호하고, 고객이 인터넷 규모의 애플리케이션을 효과적으로 구축하도록 지원하며, 웹 사이트와 인터넷 애플리케이션을 가속화하고, DDoS 공격을 막으며, 해커를 막고, Zero Trust로 향하는 고객의 여정을 지원합니다.

어떤 장치로든 1.1.1.1에 방문해 인터넷을 더 빠르고 안전하게 만들어 주는 Cloudflare의 무료 애플리케이션을 사용해 보세요.

더 나은 인터넷을 만들기 위한 Cloudflare의 사명을 자세히 알아보려면 여기에서 시작하세요. 새로운 커리어 경로를 찾고 있다면 채용 공고를 확인해 보세요.
KubernetesTerraform플랫폼 엔지니어링인프라SRE(Systems Reliability Engineer)

X에서 팔로우하기

Cloudflare|@cloudflare

관련 게시물

2026년 3월 23일

Cloudflare의 13세대 서버 출시: 캐시를 코어로 바꾸어 에지 컴퓨팅 성능 2배 향상

Cloudflare의 13세대 서버는 캐시와 코어의 균형을 다시 잡아 우리 컴퓨팅 처리량을 두 배로 늘렸습니다. 코어 수가 많은 AMD EPYC ™ Turin CPU로 이동하면서, 원시 컴퓨팅 밀도를 위해 대규모 L3 캐시를 바꿨습니다. 새로운 Rust 기반 FL2 스택을 실행함으로써 우리는 대기 시간 페널티를 완전히 완화하여 성능을 두 배나 개선했습니다....

2026년 2월 13일

ecdysis로 오래된 코드 사용하기: Cloudflare의 Rust 서비스를 위한 우아한 재시작

ecdysis는 네트워크 서비스의 다운타임 없이 업그레이드할 수 있는 Rust 라이브러리입니다. Cloudflare에서는 수백만 개의 연결을 5년 동안 보호해 온 이후, 이제 오픈 소스가 되었습니다....