[토이프로젝트] Vault 클러스터 구축: (2) 클러스터 구성하기
지난 포스팅에서 고가용성, Active-Standy 구조, Leader Election, Raft, Integrated Storage의 개념에 대해서 살펴봤습니다.
하지만, 실제 운영 환경에 필요한 안정성을 충분히 제공할 수 있는지에 대한 확신을 가질 수는 없었습니다.
그래서 Oracle VM과 Docker를 이용해 3개의 노드로 Vault 클러스터를 직접 구축해서 동작을 검증해보기로 했습니다.
1. 실습 환경
Vault 클러스터에서 사용하는 Raft 알고리즘은 과반수(쿼럼, Quorum)를 기반으로 Leader를 선출합니다. 따라서 일반적으로 홀수 개의 노드로 클러스터를 구성하는 것이 권장됩니다.
이번 실습에서는 가장 단순한 형태의 클러스터를 구축하기 위해 3개의 Vault 노드를 사용했습니다.
실습 환경은 다음과 같습니다.
| 구성 요소 | 내용 |
|---|---|
| OS | Ubuntu 24.04 LTS |
| Docker | Docker Engine 29.3.0 |
| Vault | HashiCorp Vault 1.21.4 |
| Storage | Integrated Storage (Raft) |
| Network | Docker Bridge Network |
1.1. Docker 네트워크 생성
클러스터를 구축하기 위해 각 노드는 Docker 컨테이너로 생성했습니다. 실제 운영 환경에서는 일반적으로 실제 물리 서버나 가상머신을 사용해 노드를 생성합니다. 하지만 실습을 목적으로 이러한 환경을 준비하기에는 서버 구성이나 네트워크 설정 등 준비 과정이 다소 복잡해질 수 있습니다.
Docker 컨테이너로 실행하더라도 각 Vault 노드는 서로 통신할 수 있어야 합니다. 그래야 Raft 프로토콜을 이용한 Leader Election과 데이터 복제가 정상적으로 이루어질 수 있습니다.
그래서 먼저 컨테이너들이 함께 사용할 Docker 네트워크를 생성했습니다.
$ docker network create vault-network
네트워크가 정상적으로 생성되었는지도 확인했습니다.
$ docker network ls
아래와 같이 실행 결과에서 vault-network가 조회되는 것을 확인할 수 있었습니다.
NETWORK ID NAME DRIVER SCOPE
9094e2030af0 vault-network bridge local
이후 vault-1, vault-2, vault-3이라는 이름의 컨테이너를 생성하고, 모두 vault-network에 연결하여 하나의 클러스터로 구축했습니다.
1.2. Vault 실행 모드의 선택
Vault는 다음과 같이 두 가지 실행 모드를 제공합니다.
- 개발 모드(Development Mode) :
vault server -dev명령으로 실행하며 Init과 Unseal을 자동으로 수행하여 기능을 빠르게 실습할 수 있습니다. - 서버 모드(Server Mode) :
vault server -config명령으로 실행하며 설정 파일을 이용해 Vault를 실행하는 방식으로 Storage와 Listener 등 운영에 필요한 설정을 직접 지정할 수 있습니다.
Vault에서 제공하는 대부분의 기능을 실습하는 것은 개발 모드로 실행해도 충분합니다.
하지만 Vault HA 클러스터를 구축하려면 개발 모드가 아닌 서버 모드로 실행해야 합니다. Raft를 이용한 Leader Election과 데이터 복제 기능을 사용하려면 각 노드의 Storage, API 주소, Cluster 주소 등을 직접 설정해야 하기 때문입니다.
2. 컨테이너 실행
Vault를 서버 모드로 실행하려면 HCL(HashiCorp Configuration Language) 형식의 설정 파일을 작성해야 합니다. Raft 기반의 Integrated Storage를 사용하면 데이터가 디스크에 저장됩니다. 따라서 각 노드마다 사용할 별도의 데이터 저장 디렉토리가 필요합니다.
이번 실습에서는 3개의 노드로 클러스터를 구축했습니다. 이를 위해 3개의 Vault 컨테이너에서 사용할 설정 파일(vault-1.hcl, vault-2.hcl, vault-3.hcl)과 데이터 저장 디렉토리(vault-1, vault-2, vault-3)를 준비해야 했습니다.
설정 파일과 데이터 저장 디렉토리는 편의를 위해 모두 계정의 홈 디렉터리(/home/sanghoon)에 생성했습니다.
/home/sanghoon
├── vault-1.hcl
├── vault-2.hcl
├── vault-3.hcl
├── vault-1/
├── vault-2/
└── vault-3/
2.1. Vault 설정 파일의 작성
먼저 vault-1에서 사용할 vault-1.hcl 파일을 아래와 같이 작성했습니다.
ui = true
disable_mlock = true
listener "tcp" {
address = "0.0.0.0:8200"
cluster_address = "0.0.0.0:8201"
tls_disable = true
}
storage "raft" {
path = "/vault/data"
node_id = "vault-1"
}
api_addr = "http://vault-1:8200"
cluster_addr = "http://vault-1:8201"
각 설정의 의미는 다음과 같습니다.
| 설정 | 설명 |
|---|---|
ui |
Vault Web UI를 활성화합니다. |
disable_mlock |
Docker 환경에서 메모리 잠금 기능을 비활성화합니다. |
listener |
Vault가 클라이언트 요청과 클러스터 통신을 수신할 주소를 설정합니다. |
storage "raft" |
Raft 기반의 Integrated Storage를 사용하도록 설정합니다. |
path |
Raft 데이터가 저장될 디렉토리입니다. |
node_id |
클러스터에서 각 노드를 구분하기 위한 고유 ID입니다. |
api_addr |
클라이언트와 다른 노드가 접근하는 API 주소입니다. |
cluster_addr |
Vault 노드 간 통신에 사용하는 주소입니다. |
이어서 vault-2와 vault-3에서 사용할 vault-2.hcl과 vault-3.hcl 파일도 작성했습니다.
대부분의 설정은 vault-1.hcl과 동일하지만, 아래의 값들은 컨테이너에 맞게 변경했습니다.
node_idapi_addrcluster_addr
vault-2.hcl에서 변경되는 부분
storage "raft" {
path = "/vault/data"
node_id = "vault-2"
}
api_addr = "http://vault-2:8200"
cluster_addr = "http://vault-2:8201"
vault-3.hcl에서 변경되는 부분
storage "raft" {
path = "/vault/data"
node_id = "vault-3"
}
api_addr = "http://vault-3:8200"
cluster_addr = "http://vault-3:8201"
2.2. 데이터 저장 디렉토리 생성
각 노드에서 사용할 데이터 저장 디렉토리는 아래의 명령으로 생성했습니다.
$ mkdir -p vault-1 vault-2 vault-3
2.3. 컨테이너 실행
컨테이너(vault-1, vault-2, vault-3)를 실행할 때는 앞에서 생성한 vault-network에 연결하고, 각 노드에 맞는 설정 파일과 데이터 저장 디렉토리를 마운트했습니다. 또한 메모리 잠금(mlock) 기능을 사용할 수 있도록 IPC_LOCK Capability도 함께 추가했습니다.
vault-1 컨테이너를 아래와 같이 구성하여 먼저 실행했습니다.
$ docker run -d \
--name vault-1 \
--network vault-network \
--cap-add=IPC_LOCK \
-p 8200:8200 \
-v /home/sanghoon/vault-1.hcl:/vault/config/vault.hcl \
-v /home/sanghoon/vault-1:/vault/data \
hashicorp/vault \
vault server -config=/vault/config/vault.hcl
각 Vault 컨테이너는 내부적으로 모두 8200 포트를 사용합니다. 하지만 하나의 호스트에서 여러 컨테이너를 실행하기 때문에 호스트 포트는 서로 다르게 매핑해야 합니다.
따라서 vault-2와 vault-3는 각각 8202:8200, 8204:8200로 포트를 매핑하여 실행했습니다. 반면 컨테이너 간 통신은 Docker 네트워크 내부에서 이루어지므로 설정 파일의 api_addr와 cluster_addr는 모두 컨테이너 이름과 내부 포트(8200, 8201)를 그대로 사용하면 됩니다.
이어서 vault-2 컨테이너도 아래와 같이 구성하여 실행했습니다.
$ docker run -d \
--name vault-2 \
--network vault-network \
--cap-add=IPC_LOCK \
-p 8202:8200 \
-v /home/sanghoon/vault-2.hcl:/vault/config/vault.hcl \
-v /home/sanghoon/vault-2:/vault/data \
hashicorp/vault \
vault server -config=/vault/config/vault.hcl
마지막으로 vault-3 컨테이너도 아래와 같이 구성하여 실행했습니다.
$ docker run -d \
--name vault-3 \
--network vault-network \
--cap-add=IPC_LOCK \
-p 8204:8200 \
-v /home/sanghoon/vault-3.hcl:/vault/config/vault.hcl \
-v /home/sanghoon/vault-3:/vault/data \
hashicorp/vault \
vault server -config=/vault/config/vault.hcl
2.4. 컨테이너 확인
3개의 컨테이너가 모두 정상적으로 실행되고 있는지 확인했습니다.
$ docker ps
아래와 같은 응답이 출력되었습니다. 응답을 통해 vault-1, vault-2, vault-3가 모두 실행 중(Up) 상태라는 것을 확인할 수 있었습니다.
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
fb474c8aa7ba hashicorp/vault "docker-entrypoint.s…" 4 seconds ago Up 4 seconds 0.0.0.0:8204->8200/tcp, [::]:8204->8200/tcp vault-3
3b545cd35883 hashicorp/vault "docker-entrypoint.s…" About a minute ago Up About a minute 0.0.0.0:8202->8200/tcp, [::]:8202->8200/tcp vault-2
246772fc7a1c hashicorp/vault "docker-entrypoint.s…" 21 minutes ago Up 47 seconds 0.0.0.0:8200->8200/tcp, [::]:8200->8200/tcp vault-1
3. Vault 초기화 및 Unseal
하지만, 아직은 컨테이너만 실행된 상태이며 Vault를 사용할 수 있는 상태는 아닙니다. Vault는 보안을 위해 처음 실행되면 Sealed 상태로 시작하기 때문입니다. Sealed 상태에서는 Vault에서 제공하는 보안 기능들을 사용할 수 없습니다.
그래서 클러스터를 초기화(Init)해서 각 노드를 Unseal하는데 필요한 Unseal Key를 생성해야 합니다.
3.1. 초기화
클러스터가 초기화(Init)되면 Unseal Key가 생성됩니다. 중요한 점은 vault-1, vault-2, vault-3을 각각 초기화하는 것이 아니라는 것입니다. 클러스터에 참여하는 노드 중 하나에서만 수행하면 됩니다. 이후 생성된 Unseal Key를 이용해 클러스터에 참여한 나머지 노드도 Unseal할 수 있습니다.
이번 실습에서는 vault-1 노드에서 초기화를 수행했습니다. Vault CLI를 사용하기 위해 먼저 vault-1 컨테이너 내부에 접속했습니다.
$ docker exec -it vault-1 sh
컨테이너 내부에서 Vault CLI가 요청을 보낼 Vault 서버의 주소를 지정해야 합니다. 이를 위해 VAULT_ADDR 환경변수를 아래와 같이 설정했습니다.
/ # export VAULT_ADDR=http://127.0.0.1:8200
그리고 아래 명령으로 Vault를 초기화할 수 있었습니다.
/ # vault operator init
초기화가 완료되면 Unseal Key와 Initial Root Token이 출력됩니다.
- Unseal Key : Vault를 Unseal하는 데 사용됩니다.
- Initial Root Token : 최초 관리자 권한으로 로그인할 때 사용됩니다.
Vault는 기본적으로 5개의 Unseal Key를 생성하며, 이 중 3개 이상의 키를 입력해야 Unseal이 완료됩니다
참고: Threshold=3 (기본값)
Unseal Key와 Initial Root Token은 반드시 안전한 곳에 따로 보관해야 합니다. 외부에 절대 노출하면 안됩니다.
3.2. vault-1 Unseal
초기화가 완료되고, 아래 명령으로 vault-1을 Unseal할 수 있었습니다. (주의: Unseal 명령은 컨테이너 내부에 접속한 상태에서 실행해야 합니다.)
/ # vault operator unseal
Unseal Key를 입력하라는 메시지가 출력되면 앞에서 생성된 Unseal Key 중 하나를 선택해서 입력합니다. 성공하면 아래와 같은 결과가 출력됩니다.
Key Value
--- -----
Seal Type shamir
Initialized true
Sealed true
Total Shares 5
Threshold 3
Unseal Progress 1/3
Unseal Nonce 49b1dd93-c293-8372-028c-decf6dd16ec0
Version 1.21.4
Build Date 2026-03-04T17:40:05Z
Storage Type raft
Removed From Cluster false
HA Enabled true
아직은 Unseal Key를 입력이 한번만 성공한 상태이므로 Sealed는 아직 true이고, Unseal Progress가 1/3로 표시됩니다.
같은 명령을 다시 실행하여 서로 다른 Unseal Key를 입력하는 과정을 반복합니다. 기본 설정에서는 총 3개의 서로 다른 Unseal Key를 입력하면 Vault가 Unseal 상태로 전환됩니다.
3.3. vault-2 Raft Join과 Unseal
vault-1의 컨테이너 내부에서 초기화를 실행했기 때문에, 설정 파일(vault-1.hcl)의 storage “raft” 구성을 기반으로 vault-1만 참여하는 클러스터가 생성됩니다. 아직은 하나의 노드만 존재하는 상태지만, 이후 vault-2와 vault-3이 순차적으로 참여하게 되면 최종적인 클러스터가 완성됩니다.
클러스터에 참여하지 않은 노드는 초기화 정보를 가지고 있지 않습니다. 따라서 클러스터에 참여하기 전까지는 Unseal을 위해 vault operator unseal과 같은 명령을 실행하더라도 아래와 같은 오류가 발생합니다.
Vault is not initialized
초기화 정보를 공유받기 위해 vault-2와 vault-3도 클러스터에 참여해야 합니다. 그래서 우선 vault-2 컨테이너 내부에 접속했습니다.
$ docker exec -it vault-2 sh
vault-1과 동일하게 컨테이너 내부에서 Vault CLI가 접속할 서버 주소를 지정해야 합니다. 그래서 VAULT_ADDR 환경변수의 값을 아래와 같이 설정했습니다.
/ # export VAULT_ADDR=http://127.0.0.1:8200
참고로 VAULT_ADDR는 Vault CLI가 요청을 보낼 대상을 의미합니다. 현재는 vault-2 컨테이너 내부에서 명령을 실행하고 있으므로 http://127.0.0.1:8200은 vault-2 자신입니다.
반면 클러스터 참여를 요청할때는 vault operator raft join 명령의 인수는 참여할 클러스터의 Leader 주소입니다. 클러스터의 Leader가 현재는 vault-1이므로 http://vault-1:8200을 별도로 지정해야 합니다. 그래서 아래와 같이 명령을 실행했습니다.
/ # vault operator raft join http://vault-1:8200
아래와 같이 정상적으로 클러스터에 참여(Joined)했다는 응답도 출력되었습니다.
Key Value
--- -----
Joined true
이제는 vault-1으로부터 초기화 정보가 동기화된 상태입니다. 따라서 vault-1에서 생성된 Unseal Key를 입력하면 vault-2도 Unseal 상태로 전환할 수 있었습니다.
3.4. vault-3 Raft Join과 Unseal
vault-3 역시 vault-2와 동일한 절차로 클러스터에 참여시키고 Unseal 상태로 전환할 수 있었습니다. 이 시점에서 3개의 노드는 모두 하나의 클러스터에 참여하게 됩니다.
이제 남은 것은 클러스터 상태를 조회하여 모든 노드가 정상적으로 참여했는지 확인해보는 것입니다.
4. 클러스터 참여 확인
클러스터의 상태는 아래의 명령으로 조회할 수 있었습니다. 어떤 노드에서 실행하더라도 상관은 없습니다.
/ # vault operator raft list-peers
하지만 인증 토큰이 없으면 권한 부족으로 아래와 같은 오류가 발생합니다.
Error reading the raft cluster configuration: Error making API request.
URL: GET http://127.0.0.1:8200/v1/sys/storage/raft/configuration
Code: 403. Errors:
* permission denied
그래서 컨테이너 내부에서 Initial Root Token을 아래와 같이 설정하고 클러스터 상태를 조회해야 합니다.
/ # export VAULT_TOKEN=<Initial Root Token>
명령이 성공하면 아래와 같이 응답이 출력됩니다.
Node Address State Voter
------------------------------------------------
vault-1 vault-1:8201 leader true
vault-2 vault-2:8201 follower true
vault-3 vault-3:8201 follower true
State가 leader인 vault-1이 현재 Active 노드입니다. vault-2, vault-3는 Standby 노드이므로 State가 Follower로 표시됩니다. Voter의 값이 true이면 Leader Election에 정상적으로 참여하고 있는 것을 의미합니다.
5. 다음 포스팅
하지만 이번 실습의 핵심은 클러스터를 구축하고, Leader 노드에 장애가 발생하더라도 서비스가 계속 제공될 수 있는지 확인하는 것입니다.
그래서 다음 포스팅에서는 Active 노드를 강제로 종료하여 새로운 Active가 자동으로 선출되는 과정과 Failover가 정상적으로 동작하는지 직접 검증해보겠습니다.