현 시점에서 Vault 클러스터의 구조는 다음과 같습니다.

flowchart LR
    Client["클라이언트"]

    subgraph Vault 클러스터
        V1["Vault-1<br>(장애)"]
        V2["Vault-2<br>(Active 노드)"]
        V3["Vault-3<br>(Standby 노드)"]
    end

    Client -->| http://127.0.0.1:8200 | V1
    Client -->| http://127.0.0.1:8202 | V2
    Client -->| http://127.0.0.1:8204 | V3    

이 구조에서는 클라이언트가 요청을 보낼 노드의 주소를 직접 지정해야 합니다. Standby 노드로 요청을 보내더라도 Request Forwarding 기능을 통해 Active 노드로 전달되므로 정상적인 응답을 받을 수 있습니다. 반면, 앞선 실습에서 확인했던 것처럼 장애가 발생한 노드의 주소로 요청을 보내면 연결 자체가 실패하게 됩니다.

즉, 클라이언트가 노드의 상태를 고려해 요청 대상을 직접 선택해야 하므로 실제 운영 환경에는 적합하지 않습니다. 일반적으로는 클라이언트와 Vault 클러스터 사이에 Load Balancer를 배치하는 구조를 사용합니다.


1. 구조 변경

Load Balancer를 배치하면 클라이언트는 더 이상 특정 노드가 아닌 Load Balancer의 주소로만 요청을 보내면 됩니다. 요청을 어떤 노드로 전달할지는 Load Balancer가 결정하므로 클라이언트는 어떤 노드가 Active인지, 각 노드의 주소가 무엇인지 신경 쓸 필요가 없습니다.

이번 포스팅에서는 Vault 클러스터의 구조를 아래와 같이 변경해보도록 하겠습니다.

flowchart LR
    Client["클라이언트"]
    LB["Load Balancer<br>(Nginx)"]

    subgraph Vault 클러스터
        V1["Vault-1"]
        V2["Vault-2"]
        V3["Vault-3"]
    end

    Client--> | http://127.0.0.1:8000 | LB
    LB -->| http://vault-1:8200 | V1
    LB -->| http://vault-2:8200 | V2
    LB -->| http://vault-3:8200 | V3


2. Load Balancer 구성

Load Balancer는 Nginx를 이용해 구성했습니다.

참고

Nginx를 Load Balancer로 구성하는 방법은 [토이프로젝트] Nginx로 Reverse Proxy 구성하기: (5) 로드밸런싱 기능 검증에서 다룬 적이 있습니다. 이번 포스팅을 읽기 전 먼저 참고하시면 도움이 됩니다.


2.1. Nginx 설정

nginx.conf 파일을 저장할 /home/sanghoon/nginx 디렉토리를 생성했습니다.

$ cd /home/sanghoon
$ mkdir nginx
$ cd nginx

이어서 Nginx가 Load Balancer 역할을 수행하도록 nginx.conf를 아래와 같이 작성했습니다.

events {}

http {
    upstream vault_cluster {
        server vault-1:8200;
        server vault-2:8200;
        server vault-3:8200;
    }

    server {
        listen 8000;

        location / {
            proxy_pass http://vault_cluster;
            proxy_http_version 1.1;
        }
    }
}
  • upstream : 요청을 전달할 Vault 노드들을 하나의 그룹(vault_cluster)으로 정의합니다.
  • listen 8000 : Nginx가 8000 포트에서 클라이언트의 요청을 수신합니다.
  • location / : 모든 요청을 이 설정으로 처리합니다.
  • proxy_pass : 수신한 요청을 vault_cluster에 등록된 Vault 노드 중 하나로 전달합니다.

참고

별도의 분산 알고리즘을 지정하지 않았으므로 Nginx는 기본 알고리즘인 Round Robin으로 각 노드에 요청을 분배합니다.

여기서 주의할 점이 있습니다. Nginx도 Vault 컨테이너와 동일한 Docker 네트워크에 연결되므로 upstream에는 호스트 포트가 아니라 컨테이너 내부 포트를 지정해야 합니다.

예를 들어 호스트에서는 vault-2에 8202 포트로 접근하지만 Docker 네트워크 내부에서는 vault-2:8200으로 접근합니다. vault-3도 동일하게 vault-3:8200을 사용합니다.

이는 Vault 컨테이너 내부에서는 모든 노드가 동일하게 8200 포트를 사용하기 때문입니다. vault-1.hcl, vault-2.hcl, vault-3.hcl의 api_addr 역시 이를 기준으로 설정되어 있습니다.


2.2. Nginx 컨테이너 실행

Nginx 컨테이너를 실행하려면 Docker 이미지를 먼저 준비해야 합니다.

$ docker pull nginx

컨테이너는 앞선 실습에서 Vault 클러스터 노드 간 통신을 위해 생성했던 Docker 네트워크(vault-network)에 연결했습니다. 또한 클라이언트가 8000 포트로 접근할 수 있도록 포트를 매핑하고, nginx.conf 파일을 컨테이너 내부의 설정 파일로 마운트했습니다.

$ docker run -d \
  --name nginx \
  --network vault-network \
  -p 8000:8000 \
  -v /home/sanghoon/nginx/nginx.conf:/etc/nginx/nginx.conf:ro \
  nginx

하지만 컨테이너가 정상적으로 실행되지 못하고 즉시 종료되었습니다. 원인을 찾기 위해 컨테이너의 로그를 확인했습니다.

$ docker logs nginx

로그에는 아래와 같은 오류 메시지가 출력되어 있었습니다.

/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf
10-listen-on-ipv6-by-default.sh: info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf
/docker-entrypoint.sh: Sourcing /docker-entrypoint.d/15-local-resolvers.envsh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
/docker-entrypoint.sh: Configuration complete; ready for start up
2026/07/07 12:58:16 [emerg] 1#1: host not found in upstream "vault-1:8200" in /etc/nginx/nginx.conf:5
nginx: [emerg] host not found in upstream "vault-1:8200" in /etc/nginx/nginx.conf:5

2.3. 트러블슈팅

Docker 네트워크에서는 실행 중인 컨테이너의 이름을 DNS 호스트명처럼 사용할 수 있습니다. 이전 포스팅에서 장애 상황을 재현하기 위해 vault-1 노드를 종료해 둔 상태였기 때문에 Nginx가 upstream에 등록된 vault-1을 해석하지 못했습니다. 그 결과 Nginx가 기동에 실패한 것이었습니다.

문제 해결을 위해 vault-1 노드를 다시 실행하고, Unseal까지 완료했습니다. 이 상태에서 Nginx 컨테이너를 다시 실행했습니다.

$ docker ps

주의

vault-1 노드를 다시 실행하고, 반드시 Unseal을 수행해야 정상적으로 동작합니다.

이번에는 Vault 클러스터를 구성하는 3개의 노드(vault-1, vault-2, vault-3)와 Nginx 컨테이너가 모두 정상적으로 실행 중인 것을 확인할 수 있었습니다.

CONTAINER ID   IMAGE             COMMAND                  CREATED         STATUS          PORTS                                                 NAMES
af2497e50465   nginx             "/docker-entrypoint.…"   3 seconds ago   Up 3 seconds    80/tcp, 0.0.0.0:8000->8000/tcp, [::]:8000->8000/tcp   nginx
fb474c8aa7ba   hashicorp/vault   "docker-entrypoint.s…"   10 days ago     Up 4 days       0.0.0.0:8204->8200/tcp, [::]:8204->8200/tcp           vault-3
3b545cd35883   hashicorp/vault   "docker-entrypoint.s…"   10 days ago     Up 4 days       0.0.0.0:8202->8200/tcp, [::]:8202->8200/tcp           vault-2
246772fc7a1c   hashicorp/vault   "docker-entrypoint.s…"   10 days ago     Up 15 seconds   0.0.0.0:8200->8200/tcp, [::]:8200->8200/tcp           vault-1

3. Load Balancer를 통한 시크릿 조회 요청

Load Balancer를 추가하여 Vault 클러스터의 구조를 변경하는 작업이 모두 완료되었습니다. 정상적으로 작업이 완료되었다면, Vault 클러스터의 노드(포트: 8200, 8202, 8204)가 아닌 Load Balancer(포트: 8000)에 요청을 보내고 응답을 받을 수 있어야 합니다.

3.1. 정상 상황

Vault 클러스터를 구성하는 3개의 노드 모두가 동작하고 있는 상태에서 Load Balancer에 시크릿 조회를 요청해봤습니다.

$ curl \
 --header "X-Vault-Token: $VAULT_TOKEN" \
 http://127.0.0.1:8000/v1/secret/data/vault-test

시크릿이 정상적으로 조회되면서 아래와 같은 응답이 반환되었습니다.

{
    "request_id":"d9bdea74-44f1-0aad-f68d-d02e0149199b",
    "lease_id":"",
    "renewable":false,
    "lease_duration":0,"data":
    {
        "data":
        {
            "blog":"https://sanghoon-lee.github.io",
            "name":"sanghoon"
        },
        "metadata":
        {
            "created_time":"2026-06-29T11:52:28.836931496Z",
            "custom_metadata":null,
            "deletion_time":"",
            "destroyed":false,
            "version":1
        }
    },
    "wrap_info":null,
    "warnings":null,
    "auth":null,
    "mount_type":"kv"
}

이를 통해 클라이언트가 노드의 주소를 직접 지정하지 않고도 Load Balancer를 통해 시크릿을 정상적으로 조회할 수 있음을 확인할 수 있었습니다.


3.2. 단일 노드의 장애 상황

하지만, 아직 노드에 장애가 발생하는 경우에 정상적으로 Load Balancer가 응답을 반환할 수 있는지는 확인해보지 못했습니다.

그래서 vault-2 노드를 종료시켜 장애 상황을 재현했습니다.

$ docker kill vault-2

Active 노드인 vault-2가 종료된 상태였지만, Load Balancer에 시크릿 조회를 요청해서 정상적인 응답을 받을 수 있었습니다.

$ curl \
 --header "X-Vault-Token: $VAULT_TOKEN" \
 http://127.0.0.1:8000/v1/secret/data/vault-test
{
    "request_id":"8cfc263e-52bb-e100-e1aa-72aead06cd5e",
    "lease_id":"",
    "renewable":false,
    "lease_duration":0,
    "data":
    {
        "data":
        {
            "blog":"https://sanghoon-lee.github.io",
            "name":"sanghoon"
        },
        "metadata":
        {
            "created_time":"2026-06-29T11:52:28.836931496Z",
            "custom_metadata":null,
            "deletion_time":"",
            "destroyed":false,
            "version":1
        }
    },
    "wrap_info":null,
    "warnings":null,
    "auth":null,
    "mount_type":"kv"
}

3.3. 다중 노드의 장애 상황

이어서 vault-3 노드도 종료했습니다.

$ docker kill vault-3

이제는 Vault 클러스터를 구성하고 있는 3개의 노드 중에서 2개에 장애가 발생한 상태가 되었습니다. 이 상태에서도 Load Balancer를 통해 시크릿을 조회할 수 있는지 확인해봤습니다.

$ curl \
 --header "X-Vault-Token: $VAULT_TOKEN" \
 http://127.0.0.1:8000/v1/secret/data/vault-test
$

이번에는 시크릿 조회를 요청하더라도 아무런 응답도 출력되지 않았습니다. 바로 Raft 쿼럼(Quorum) 상실 때문입니다.


4. Raft 쿼럼(Quorum) 상실

3개의 노드로 구성된 Vault 클러스터가 정상적으로 합의를 수행하려면 서로 통신 가능한 노드가 과반수인 2개 이상이어야 합니다.

  • 3개 노드 중 1개 종료 → 2개 생존 → 쿼럼 유지 → 서비스 가능
  • 3개 노드 중 2개 종료 → 1개 생존 → 쿼럼 상실 → 서비스 중단

새로운 Leader를 선출하거나 Raft 로그를 확정하려면 과반수의 동의가 필요하기 때문입니다. 남아 있는 하나의 노드만으로는 과반수를 확보할 수 없기 때문에 새로운 Leader를 선출할 수 없습니다.

flowchart LR
    subgraph "3노드 클러스터"
        V1["vault-1<br/>생존"]
        V2["vault-2<br/>종료"]
        V3["vault-3<br/>종료"]
    end

    V1 --> Q["과반수 미달<br/>Leader 선출 불가 및 로그 확정 불가"]
    Q --> F["시크릿 조회 불가"]

Load Balancer는 정상적인 노드로 요청을 전달하는 역할만 수행합니다. Vault 클러스터의 쿼럼이 상실된 상태까지 복구해주는 것은 아닙니다. 따라서 이런 경우에는 남아 있는 노드로 요청이 전달되더라도 응답을 받을 수 없습니다.

참고로 두 개 노드의 장애까지 허용하려면 최소 다섯 개의 노드로 Vault 클러스터를 구성해야 합니다.

노드 수 필요한 과반수 허용 가능한 장애
3개 2개 1개
5개 3개 2개
7개 4개 3개

5. 마무리

이번 포스팅 시리즈에서는 Vault 클러스터를 구축하면서 Leader Election, Request Forwarding, 장애 복구(Failover), 그리고 Load Balancer를 이용한 단일 진입점까지 직접 구성하고 검증했습니다. 이를 통해 Vault 클러스터가 장애 상황에서 서비스를 지속하기 위해 어떻게 동작하는지 전반적인 구조와 원리를 이해할 수 있었습니다.

특히 Load Balancer는 클라이언트의 접근을 단순화하고 개별 노드 장애에 대응할 수 있도록 도와주지만, Vault 클러스터의 쿼럼 상실까지 해결해주는 것은 아닙니다. 따라서 안정적인 운영을 위해서는 예상하는 장애 범위를 고려해 적절한 수의 노드로 클러스터를 구성하고, 항상 쿼럼이 유지될 수 있도록 설계하는 것이 중요합니다.