복제
복제 (Replication, Vault Enterprise)
Vault Enterprise 0.7부터 다중 데이터센터 복제를 지원합니다. 이 기능을 사용하기 전에 의도된 유스케이스, 설계 목표, 상위 수준 아키텍처를 이해하는 것이 유용합니다. 복제는 프라이머리/세컨더리(1:N) 모델과 비동기 복제에 기반하며, 글로벌 배포를 위한 고가용성에 초점을 맞춥니다. 설계와 구현에서 이루어진 트레이드오프는 이러한 상위 수준 목표를 반영합니다.
출처: 문서
본문
유스케이스 (Use cases)
Vault 복제는 여러 일반적인 유스케이스에 기반합니다:
- 다중 데이터센터 배포 (Multi-Datacenter Deployments): 많은 데이터센터에 걸쳐 고가용성 방식으로 애플리케이션에 Vault를 제공하는 것은 흔한 과제입니다. 단일 Vault 클러스터를 실행하면 원격 클라이언트의 접근 지연이 높아지고, 연결 실패 시 가용성 손실이나 정전이 발생하며, 확장성이 제한됩니다.
- 백업 사이트 (Backup Sites): 프라이머리 데이터센터 손실에 대해 견고한 업무 연속성 계획을 구현하려면 핫 백업 사이트로 빠르고 쉽게 장애 조치할 수 있는 능력이 필요합니다.
- 처리량 확장 (Scaling Throughput): Encryption-as-a-Service나 암호화 오프로드에 Vault를 사용하는 애플리케이션은 Vault에 매우 높은 양의 요청을 생성할 수 있습니다. 여러 클러스터 간에 키를 복제하면 추가 서버에 로드를 분산해 요청 처리량을 확장할 수 있습니다.
설계 목표 (Design goals)
Vault 복제의 유스케이스에 기반해 구현에는 여러 설계 목표가 있었습니다:
- 가용성 (Availability): Vault의 글로벌 배포는 높은 수준의 가용성을 요구하며 감소된 일관성은 견딜 수 있습니다. 완전한 연결 상태에서 복제는 프라이머리와 세컨더리 클러스터 간에 거의 실시간입니다. 프라이머리와 세컨더리 사이의 저하된 연결은 프라이머리가 요청을 처리하는 능력에 영향을 주지 않으며, 세컨더리는 마지막으로 알려진 데이터에 대한 읽기를 계속 제공합니다.
- 충돌 없음 (Conflict Free): 특정 복제 기법은 잠재적 쓰기 충돌이 일어나게 합니다. 특히 여러 사이트에서 쓰기가 허용되는 active/active 구성은 충돌 해결 전략을 요구합니다. 이는 last-write-wins처럼 데이터 손실을 허용하는 기법부터 키당 여러 값을 허용하는 것처럼 수동 운영자 해결을 요구하는 기법까지 다양합니다. 우리는 데이터 손실이나 수동 개입이 없도록 충돌 가능성을 피합니다.
- 클라이언트에 투명함 (Transparent to Clients): Vault 복제는 Vault 클라이언트에 투명해야 하므로 기존 얇은 클라이언트(thin client)가 수정 없이 작동합니다. Vault 서버가 필요할 때 프라이머리로 요청을 전달하는 로직을 처리하며, 요청이 처리되도록 내부적으로 멀티홉 라우팅이 수행됩니다.
- 운영이 단순함 (Simple to Operate): 복제 클러스터 운영은 관리 오버헤드와 잠재적 보안 공백을 피하기 위해 단순해야 합니다. 복제 설정은 매우 간단하며, 세컨더리는 프라이머리보다 임의로 뒤처지는 것을 처리할 수 있어 운영자가 데이터를 복사하거나 프라이머리를 스냅샷하는 개입이 필요 없습니다.
아키텍처 (Architecture)
Vault 복제의 아키텍처는 설계 목표에 기반하며 의도된 유스케이스에 초점을 맞춥니다. 복제가 활성화되면 클러스터는 프라이머리 또는 세컨더리로 설정됩니다. 프라이머리 클러스터는 권위(authoritative)가 있으며, 정책이나 시크릿을 수정하는 것처럼 기본 데이터 저장소에 쓰는 작업을 수행할 수 있는 유일한 클러스터입니다. 세컨더리 클러스터는 시크릿 읽기나 transit을 통한 데이터 전송 같은 다른 모든 작업을 처리할 수 있으며, 쓰기는 프라이머리 클러스터로 전달합니다. 여러 프라이머리를 허용하지 않음으로써 클러스터가 충돌이 없고 권위 있는 상태를 갖도록 보장합니다.
프라이머리 클러스터는 로그 전달(log shipping)을 사용해 변경 사항을 모든 세컨더리에 복제합니다. 이렇게 하면 완전한 네트워크 연결일 때 쓰기가 거의 실시간으로 전역에 보이게 됩니다. 세컨더리가 다운되거나 프라이머리와 통신할 수 없으면 프라이머리에서 쓰기가 차단되지 않고 세컨더리에서 읽기는 계속 제공됩니다. 이것은 Vault의 가용성을 보장합니다. 세컨더리가 초기화되거나 저하된 연결에서 복구되면 프라이머리와 자동으로 조정(reconcile)합니다.
마지막으로 클라이언트는 두꺼운 클라이언트 없이 어느 Vault 서버와도 통신할 수 있습니다. 클라이언트가 standby 인스턴스와 통신하면 요청은 자동으로 active 인스턴스로 전달됩니다. 세컨더리 클러스터는 읽기를 로컬로 처리하고 쓰기 요청은 프라이머리 클러스터로 전달합니다. 프라이머리 클러스터는 모든 요청 유형을 처리할 수 있습니다.
Vault가 하는 중요한 최적화는 클러스터 간에 토큰이나 임대를 복제하지 않는 것입니다. 정책과 시크릿은 Vault가 관리하는 데이터의 소수이며 비교적 안정적입니다. 토큰과 임대는 생성과 만료가 빠르게 일어나 훨씬 동적입니다. 토큰과 임대를 로컬에 유지하면 복제해야 할 데이터 양이 줄어들고, 클러스터 간에 TTL 관리 작업이 분산됩니다. 단, 클라이언트가 통신하는 Vault 클러스터를 전환하면 재인증해야 합니다.
구현 세부사항 (Implementation details)
복제의 상위 수준 아키텍처를 이해하는 것은 트레이드오프가 유스케이스에 적절한지 확인하는 데 중요합니다. 구현 세부사항은 성능 특성이나 실패 시나리오에 대해 궁금하거나 더 알고 싶은 사람에게 유용할 수 있습니다.
복제를 사용하려면 Consul처럼 트랜잭션 업데이트를 지원하는 스토리지 백엔드가 필요합니다. 이렇게 하면 여러 키/값 업데이트를 원자적으로 수행할 수 있습니다. 복제는 이것을 사용해 모든 업데이트의 Write-Ahead-Log(WAL)를 유지하므로 키 업데이트가 WAL 항목 생성과 원자적으로 일어납니다. WAL은 이후 Vault 클러스터 간에 로그 전달을 수행하는 데 사용됩니다. 세컨더리가 프라이머리와 밀접하게 동기화되면 Vault는 적용할 새 WAL을 직접 스트리밍해 거의 실시간 복제를 제공합니다. 세컨더리를 위해 제한된 WAL 집합이 유지되며, 오래된 WAL은 자동으로 가비지 컬렉션됩니다.
세컨더리가 초기화되거나 프라이머리보다 너무 뒤처지면 동기화할 WAL이 충분하지 않을 수 있습니다. 이 시나리오를 처리하기 위해 Vault는 암호화 키의 merkle 인덱스를 유지합니다. 키가 업데이트되거나 삭제될 때마다 merkle 인덱스는 변경을 반영하도록 업데이트됩니다. 세컨더리가 프라이머리와 조정해야 하면 merkle 인덱스를 비교해 어떤 키가 동기화에서 벗어났는지 판단합니다. 인덱스 구조 덕분에 이것은 매우 효율적으로 수행되며, 보통 왕복 두 번과 적은 양의 데이터만 필요합니다. 세컨더리는 이 정보로 조정한 뒤 WAL 스트리밍 모드로 전환합니다.
성능은 Vault에서 중요한 관심사이므로 WAL 항목은 배치되고 merkle 인덱스는 모든 작업마다 디스크에 플러시되지 않습니다. 대신 인덱스는 매 작업마다 메모리에서 업데이트되고 비동기적으로 디스크에 플러시됩니다. 결과적으로 충돌이나 정전은 merkle 인덱스가 기본 키와 동기화되지 않게 할 수 있습니다. Vault는 ARIES 복구 알고리즘을 사용해 그러한 실패 조건에서 인덱스의 일관성을 보장합니다.
로그 전달은 전통적으로 WAL 스트림이 동기화되어야 하므로, 새 프라이머리 클러스터가 승격될 때 추가 복잡성이 발생할 수 있습니다. Vault는 merkle 인덱스를 진실의 원천으로 사용하므로 WAL 스트림이 완전히 분리되고 동기화되지 않아도 됩니다. 이것은 운영자가 Vault 복제 관리를 단순화합니다.
주소 지정 (Addressing)
프라이머리의 클러스터 주소 (Cluster addresses on the primary)
클러스터가 복제 프라이머리로 활성화되면 core/cluster/replicated/info 또는 core/cluster/replicated-dr/info 아래의 스토리지에 클러스터 정의를 영속화합니다. 클러스터 정의의 선택 필드는 primary_cluster_addr 이며 활성화 요청에서 제공할 수 있습니다.
성능 standby는 active 노드에 정기적으로 하트비트 RPC 요청을 보내며, RPC 인자 중 하나는 로컬 노드의 cluster_addr 입니다. 프라이머리 active 노드는 피어로부터 받은 이러한 클러스터 주소를 15초 만료 시간의 clusterPeerClusterAddrsCache 라는 메모리 내 캐시에 보관합니다.
세컨더리의 클러스터 주소 (Cluster addresses on the secondary)
세컨더리가 활성화되면 그 복제 활성화 토큰(프라이머리에서 얻음)은 primary_cluster_addr 필드를 포함합니다. 이것은 프라이머리가 활성화될 때 생성된 영속화된 클러스터 정의에서 가져오며, 또는 그때 primary_cluster_addr 이 제공되지 않았다면 토큰은 활성화 토큰 생성 시점의 프라이머리 내 현재 active 노드의 cluster_addr 을 포함합니다.
세컨더리는 자신의 클러스터 정의 버전을 다시 core/cluster/replicated/info 또는 core/cluster/replicated-dr/info 의 스토리지에 영속화합니다. 여기서 primary_cluster_addr 필드는 활성화 토큰에서 얻은 것입니다.
세컨더리 active 노드는 프라이머리 active 노드에 정기적으로 하트비트 RPC 요청을 보냅니다. 이에 대한 응답으로 프라이머리는 ClusterAddrs 필드(자신의 clusterPeerClusterAddrsCache 내용과 현재 active 노드의 cluster_addr 을 포함)를 포함한 응답을 반환합니다. 세컨더리는 그 응답으로 메모리 내 known_primary_cluster_addrs 기록을 갱신하고, 추가로 core/primary-addrs/dr 또는 core/primary-addrs/perf 아래의 스토리지에 영속화합니다. 이때 Vault는 응답에서 얻은 ClusterAddrs 값을 포함해 "replication: successful heartbeat" 행을 로그에 기록합니다.
세컨더리 RPC 주소 해석 (Secondary RPC address resolution)
gRPC에는 RPC 요청을 수행하는 데 사용할 대상 주소 목록이 주어집니다. gRPC는 성능 standby가 대부분의 RPC를 처리할 수 없다는 것을 발견하고, active 노드 클러스터 주소 외에는 신속히 걸러냅니다. 프라이머리 active 노드가 바뀌면 gRPC는 그 주소가 더 이상 유효하지 않음을 알게 되고, 알려진 대상 주소 중 하나라면 새 active 노드로 자동으로 장애 조치합니다.
세컨더리는 몇 초마다 프라이머리를 위한 gRPC 대상 주소 목록을 구축하는 백그라운드 해석기(resolver) goroutine을 실행합니다. 그 출력은 "loaded addresses" 로 Trace 수준에서 기록됩니다.
프라이머리 클러스터 주소 목록을 구축하려면 해석기 goroutine은 보통 known_primary_cluster_addrs 를 스토리지에 있는 클러스터 정의의 primary_cluster_addr 과 연결하면 됩니다.
이런 일반 동작에는 두 가지 예외가 있습니다: goroutine이 루프를 처음 돌 때와, gRPC가 강제 ResolveNow를 요청할 때(대상 주소 중 어느 것으로도 RPC를 수행할 수 없을 때 발생)입니다. 이 두 경우 모두 해석기 goroutine은 프라이머리에 특별한 RemoteResolve RPC를 발행합니다. 이 RPC는 다른 모든 복제 RPC와 달리 성능 standby와 active 노드 모두가 처리할 수 있다는 점에서 특별합니다. 어느 경우든 노드는 프라이머리 클러스터 정의에 저장된 primary_cluster_addr 을 반환하거나, 그것이 없으면 현재 active 노드의 클러스터 주소를 반환합니다. RemoteResolve 호출 결과는 해석기가 프라이머리에 대한 일반 RPC용 gRPC에 주는 대상 주소 목록에 포함됩니다.
주의 사항 (Caveats)
- 클러스터 버전 불일치 (Mismatched Cluster Versions): 새 버전의 Vault에서 이전 버전으로 복제하는 것은 안전하지 않습니다. 복제된 클러스터를 업그레이드할 때 업스트림 클러스터가 항상 다운스트림 클러스터보다 이전 Vault 버전인지 확인하세요. 예는 Upgrading Vault를 참고하세요.
- 쓰기 후 읽기 일관성 (Read-After-Write Consistency): 잠재적 충돌을 피하기 위해 모든 쓰기 요청은 세컨더리에서 프라이머리 클러스터로 전달됩니다. 복제는 거의 실시간이지만 즉각적이지는 않으므로, 클라이언트가 세컨더리에 쓰고 이후 읽기가 이전 값을 반환할 가능성이 있습니다. 세컨더리는 쓰기가 복제되거나 타임아웃(2초)에 도달할 때까지 쓰기 요청을 지연시켜 개별 클라이언트의 후속 요청에서 이를 가리려 합니다. 타임아웃에 도달하면 클라이언트는 경고를 받습니다. 클라이언트도 이를 방지하기 위한 조치를 취할 수 있습니다. Consistency를 참고하세요.
- 오래된 읽기 (Stale Reads): 세컨더리 클러스터는 로컬로 복제된 데이터에 기반해 읽기를 처리합니다. 정상 작동 중에는 프라이머리 업데이트를 세컨더리가 거의 실시간으로 받습니다. 그러나 정전이나 네트워크 서비스 중단 중에는 복제가 멈추고 세컨더리가 오래된 데이터를 가질 수 있습니다. 정전이 복구되면 클러스터는 자동으로 복구하고 오래된 데이터를 조정하지만, 그 사이의 읽기는 오래된 데이터를 받을 수 있습니다.
- 성능 복제의 동적 시크릿 (Dynamic secrets with performance replication): 세컨더리 클러스터에서 취소 불가능한(irrevocable) 임대를 수동으로 폐기하지 않고 동적 시크릿 엔진을 비활성화하면 성능 복제가 깨집니다. 예를 들어 프라이머리 클러스터에서 database 시크릿 플러그인을 안전하게 비활성화하려면 먼저 세컨더리 클러스터의 로컬 임대를 모두 폐기해야 합니다.