Secrets Manager 성능
GitLab v19.5요약
Secrets Manager 코드는 OpenBao와 CustomersDot라는 두 원격 서비스를 호출합니다. job을 가져올 때 CustomersDot 오류는 로그에 기록되고 빌드는 예약됩니다. 프레젠터는 빌드마다 프로젝트 서버 블록을 한 번 만듭니다.
Secrets Manager 코드는 OpenBao와 CustomersDot라는 두 원격 서비스를 호출합니다. 성능 문제는 대부분 핫 패스(요청마다, CI job마다 실행되는 것처럼 매우 자주 실행되는 코드)에 이 호출을 추가하거나, 시크릿 수에 비례해 늘어나는 작업에서 발생합니다. Secrets Manager 코드를 변경할 때는 이러한 비용을 염두에 둡니다. 일반적인 조언은 머지 리퀘스트 성능 가이드라인을 참조합니다.
핫 패스#
| 경로 | Secrets Manager 작업 | 비용 제어 |
|---|---|---|
CI job 가져오기, EE::Ci::RegisterJobService#secrets_manager_access_denied? |
gitlab_secrets_manager 시크릿을 요청하는 빌드에 대한 라이선스, 사용 가능 여부, 권한(entitlement) 확인 |
CustomersDot 호출당 HTTP 제한 시간 1초, 공유 캐시 1분. 오류가 나면 허용합니다(fail open). |
러너 페이로드, EE::Ci::BuildRunnerPresenter |
사용 가능 여부와 권한 확인, Secrets Manager 조회, CI JWT 발급 | 확인 결과를 프로젝트당 한 번, 서로 다른 그룹당 한 번 메모이제이션합니다. 같은 1분 캐시. 오류가 나면 차단합니다(fail closed). |
사이드바 메뉴 항목, ChecksNavReadCapability |
시크릿을 읽을 수 있는 사용자가 프로젝트 또는 그룹 페이지를 렌더링할 때마다 OpenBao 기능 조회 | OpenBao 제한 시간 3초, 사용자와 Secrets Manager별 30초 캐시 |
시크릿 페이지, Projects::SecretsController#check_read_capability! |
같은 기능 조회 | 같은 30초 캐시, 제한 시간 없음 |
| GraphQL 시크릿 목록 및 개수 | 요청마다 OpenBao LIST |
없음 |
job을 가져올 때 CustomersDot 오류는 로그에 기록되고 빌드는 예약됩니다.
이후 프레젠터가 기본 제한 시간으로 권한을 다시 확인하며, 접근이 거부되면 SecretsManagement::Entitlement::AccessDeniedError를 발생시킵니다.
register job 서비스는 이 오류를 secrets_manager_access_denied 실패 사유로 매핑합니다.
프레젠터는 빌드마다 프로젝트 서버 블록을 한 번 만듭니다. 그룹 서버 블록은 그룹 시크릿마다 만들어지므로, 그룹 시크릿마다 새 CI JWT를 발급하고 내부 이벤트를 추적합니다.
러너는 시크릿 값을 OpenBao에서 직접 읽습니다. Rails는 감사 로그 콜백을 통해서만 각 읽기를 확인합니다.
OpenBao 왕복 호출#
모든 요청은 인라인으로 인증하므로, OpenBao는 호출마다 JWT를 검증합니다. 자세한 내용은 Secrets Manager OpenBao 클라이언트 및 인증을 참조합니다.
| 작업 | OpenBao 호출 | 참고 |
|---|---|---|
| 생성 | 7 | 개수 LIST(한도가 0이면 건너뜀), 메타데이터 읽기, 값 쓰기, 메타데이터 쓰기, CI 정책 읽기 및 쓰기, 최종 메타데이터 쓰기 |
| 업데이트, 범위 변경 없음 | 5 또는 6 | 메타데이터 읽기, 메타데이터 쓰기, 값 쓰기(값이 주어진 경우에만), CI 정책 읽기 및 쓰기, 최종 메타데이터 쓰기 |
| 업데이트, 환경 또는 브랜치 변경 | 7~9 | 전체 시크릿 LIST와 이전 정책의 삭제 또는 재작성이 추가됩니다 |
| 삭제 | 4 또는 5 | 메타데이터 읽기, 삭제, 전체 시크릿 LIST, 이어서 정책 삭제 또는 정책 읽기 및 쓰기 |
| 목록 | 1 | 상세 메타데이터 경로에 대한 LIST 1회, 로테이션 필드를 선택하면 데이터베이스 쿼리 1회 추가 |
| 시크릿 하나 읽기 | 1 | 로테이션 레코드에 대한 데이터베이스 쿼리가 최대 2회 추가됩니다 |
| 개수 | 1 | 모든 키의 LIST. 한도 확인은 limit + 1을 전달하므로 OpenBao가 일찍 멈춥니다. |
| 유효 기능 | 3 | 로그인, sys/capabilities-self, 이어서 토큰 폐기 |
목록은 OpenBao에서 페이지로 나뉘지 않습니다.
ProjectSecrets::ListService와 GroupSecrets::ListService는 모든 시크릿을 불러오고, GraphQL 연결이 메모리에서 배열을 페이지로 나눕니다.
로테이션이 필요한 시크릿을 나열하는 서비스는 로테이션 레코드와 함께 전체 목록을 불러온 뒤 Ruby에서 필터링하고 정렬합니다.
SecretsManagerClient는 timeout: 옵션을 받지만 시크릿 서비스는 이를 설정하지 않습니다.
이러한 호출은 연결과 읽기에 Net::HTTP 기본값인 60초를 사용합니다.
사이드바의 기능 조회만 제한 시간을 설정합니다.
CI 정책 갱신 비용#
CiPolicies::ProjectSecretRefresher와 CiPolicies::GroupSecretRefresher는 생성, 업데이트, 삭제 때마다 실행됩니다.
동작 방식은 Secrets Manager 서비스를 참조합니다.
- 정책에 시크릿을 추가하면 전체 액세스 제어 목록(ACL) 정책을 읽고 다시 씁니다. 정책에는 해당 범위의 시크릿마다 두 개의 경로가 있으므로, 정책의 크기는 이를 공유하는 시크릿 수에 따라 늘어납니다.
- 환경과 브랜치가 바뀌지 않아도 업데이트는 정책을 다시 씁니다.
- 정책에서 시크릿을 제거하면
count_secrets_for_policy를 호출하며, 이 메서드는 메타데이터와 함께 Secrets Manager의 모든 시크릿을 나열합니다. 따라서 삭제와 범위 변경의 비용은 O(시크릿 수)입니다.
배타적 리스#
시크릿 생성, 업데이트, 삭제는 프로젝트 또는 그룹마다 하나의 리스 키를 공유합니다.
권한 업데이트와 삭제, 프로비저닝, 프로비저닝 해제도 마찬가지입니다.
각 리스에는 아래 표에 나온 제한 시간, 즉 TTL(time to live)이 있습니다.
Helpers::ExclusiveLeaseHelper#with_exclusive_lease_for는 retries: 0을 사용하므로, 두 번째 호출자는 즉시 "Another secret operation in progress"를 받습니다.
요청은 대기열에 들어가지 않습니다.
부하 상태에서 하나의 프로젝트나 그룹에 대한 병렬 쓰기는 실패하고, 서로 다른 프로젝트나 그룹에 대한 쓰기는 서로를 차단하지 않습니다.
| 호출자 | 리스 TTL |
|---|---|
| 시크릿 및 권한 쓰기 | 30초(DEFAULT_LEASE_TIMEOUT) |
| 프로비저닝 | 120초 |
프로비저닝 해제(BaseDeprovisionService::LEASE_TIMEOUT) |
120초 |
쓰기 하나는 클라이언트 제한 시간 없이 최대 아홉 번 OpenBao를 호출합니다. OpenBao가 멈추면 쓰기가 30초 리스를 넘겨 계속 실행될 수 있고, 그러면 두 번째 쓰기가 시작될 수 있습니다.
대량 처리 경로#
- 그룹 이전.
EE::Groups::TransferService#bulk_initiate_secrets_manager_deprovisions는GROUP_QUERY_BATCH_SIZE와PROJECT_QUERY_BATCH_SIZE(둘 다 1000)의each_batch로 하위 트리를 순회합니다. 각 배치는bulk_initiate_for_groups와bulk_initiate_for_projects를 통해active상태 Secrets Manager에delete_all을 한 번 실행합니다. 데이터베이스 트리거가 행마다 태스크를 삽입합니다. 이 코드는 이전 트랜잭션 안에서 실행되므로 Sidekiq job은 큐에 넣지 않습니다. 레코드별 서비스 호출을 사용하면 요청 안에서 하위 항목마다 여러 쿼리와 큐 등록이 한 번씩 실행됩니다. - 프로젝트 이전은 레코드 하나의 프로비저닝을 해제합니다.
- 프로젝트와 그룹 삭제는
ON DELETE CASCADE와 트리거에 의존합니다. - 유지 관리 및 리퍼 cron은 태스크를 배치로 처리하고 태스크별로 오류를 격리합니다. 상수는 Secrets Manager 워커 및 정책을 참조합니다.
ReconcileNamespaceSecretCountsCronWorker는 Secrets Manager마다 워커 하나를 500개 단위 배치로 큐에 넣으며, 최대 1시간의 무작위 지연으로 분산합니다.EmitSecretsStoredBillableEventCronWorker는 루트 네임스페이스 500개 단위 배치로bulk_perform_async_with_contexts를 사용합니다.
CustomersDot과 권한#
상태, 캐시, 대체 동작은 Secrets Manager 이행 및 권한을 참조합니다. 성능 측면은 다음과 같습니다.
- 캐시되지 않은 해석 한 번은 순차적인 HTTP 호출 두 번(체험판 조회, 이어서 컨슈머 해석)을 수행합니다.
Gitlab::SubscriptionPortal::Client는 각 응답을Cache-Controlmax-age 동안 캐시하며, max-age가 없으면 120초(CDOT_CACHE_FALLBACK_TTL) 동안 캐시합니다. max-age가0이면 캐시하지 않습니다.- job 가져오기와 러너 페이로드 확인은 모두
JOB_PICKUP_CACHE_TTL(1분)로 설정한cache_ttl을 전달합니다. Rails 정책, 뮤테이션, UI 헬퍼는 자체적인 요청 간 캐시 없이 권한을 해석합니다. - 실시간 해석은 매번 마지막으로 확인된 정상 레코드도 Redis에 씁니다.
| 호출자 | 호출당 HTTP 제한 시간 |
|---|---|
기본 Entitlement.for |
기능 플래그 뒤에서는 2초, 그렇지 않으면 Gitlab::HTTP 기본값 |
| job 가져오기 확인 | 1초 |
secrets_read 이미터 |
0.25초 |
뮤테이션의 Entitlement.for! |
Gitlab::HTTP 기본값 |
Net::HTTP는 GET에서 읽기 제한 시간 초과를 한 번 재시도하므로, 멈춘 호출은 제한 시간의 약 두 배가 걸릴 수 있습니다.
개수와 한도#
| 개수 | 출처 | 시점 |
|---|---|---|
| 생성 시 한도 확인 | limit + 1을 사용한 OpenBao LIST |
생성할 때마다 |
| GraphQL 시크릿 개수 | OpenBao LIST |
요청마다 |
NamespaceSecretCount |
데이터베이스 행 | secrets_stored 청구 경로에서만 읽음 |
ReconcileNamespaceSecretCountWorker는 OpenBao LIST 한 번과 삽입 또는 업데이트 한 번으로 NamespaceSecretCount를 갱신합니다.
생성과 삭제 후, 그리고 일일 cron에서 실행됩니다.
예약된 job을 포함해 네임스페이스별로 중복이 제거되며, 데이터베이스 상태 신호가 나빠지면 실행을 미룹니다.
감사 로그와 청구 이벤트#
OpenBao는 자체 요청에 응답하기 전에 POST /internal/secrets_manager/audit_logs의 응답을 기다립니다.
이 엔드포인트는 요청 유형 줄을 건너뛰고 AuditLogWorker를 큐에 넣습니다.
AuditLogWorker는 긴급도가 낮고, 원시 페이로드를 기준으로 중복이 제거되며, 감사 테이블이 정상이 아니면 실행을 미룹니다.- 처리된 읽기마다 청구 메타데이터를 위해 권한을 해석하므로 CustomersDot을 호출할 수 있습니다.
- 이 엔드포인트는 1MB 이상의 본문을 거부합니다.
속도 제한#
권한 갱신 뮤테이션은 자체 애플리케이션 속도 제한이 있는 유일한 Secrets Manager 엔드포인트이며, secrets_manager_entitlement_refresh로 사용자당 분당 10회입니다.
각 갱신은 CustomersDot 캐시를 건너뛰고 실시간 CustomersDot 호출을 두 번 수행합니다.
그 밖의 Secrets Manager 엔드포인트나 뮤테이션에는 자체 속도 제한이 없습니다.
액세스 토큰 REST 엔드포인트는 JWT를 로컬에서 발급하며 OpenBao를 호출하지 않습니다.
성능 검토 체크리스트#
- 변경 사항이 CI job 가져오기, 러너 페이로드, 사이드바에 OpenBao 또는 CustomersDot 호출을 추가하는지 확인합니다.
- 지연 시간에 민감한 경로의 새 호출이 제한 시간을 설정하는지, 그리고 의도적으로 fail open 또는 fail closed로 동작하는지 확인합니다.
- 변경 사항이 시크릿, 프로젝트, 그룹을 순회하는 반복문 안에 호출이나 데이터베이스 쿼리를 추가하는지 확인합니다.
- 새 쓰기 경로가 리프레셔의 전체
LIST처럼 시크릿 수에 비례해 늘어나는 작업을 추가하는지 확인합니다. - 새 쓰기가 엔터티별 리스를 사용하는지, 그리고 리스 TTL보다 충분히 일찍 끝날 수 있는지 확인합니다.
- 핫 패스의 새 권한 호출자가
cache_ttl을 전달하는지, 그리고 대응하는 뮤테이션이 캐시를 지우는지 확인합니다. - 대량 처리 경로가 일정한 배치 크기의
each_batch를 사용하며, 트랜잭션 안에서 레코드별 서비스 호출을 피하는지 확인합니다. - 새 개수가 목적에 맞는 출처를 읽는지 확인합니다. 한도에는 OpenBao를, 청구에는
NamespaceSecretCount를 사용합니다.