z/OS zCX 클러스터에서 Vault 실행
z/OS zCX 클러스터에서 Vault 실행 (Run Vault on an IBM z/OS zCX cluster)
IBM z/OS Container Extensions(zCX)에 완전히 보안된 3노드 HashiCorp Vault Enterprise 클러스터를 배포하는 방법을 다룹니다.
출처: 문서
본문
IBM z/OS Container Extensions(zCX)에 완전히 보안된 3노드 HashiCorp Vault Enterprise 클러스터를 배포해 보아요.
시작하기 전에
- Vault 봉인·봉인 해제(sealing and unsealing)에 대해 읽어보세요. Vault를 봉인 해제하지 않으면 배포를 테스트할 수 없어요.
- Docker CLI를 설치 하세요.
- 권한을 확인하세요. Docker 컨테이너 배포·업데이트, vault operator 명령 실행, 로드밸런서 구성·배포 권한이 있어야 해요.
- 암호화 인증서가 있어야 해요. 클러스터에서 Vault 클라이언트, HAProxy, 다른 노드 간의 통신을 보호하려면 다음 인증서가 필요합니다.
- vault.pem — 노드 간 통신을 보호하는 TLS 인증서
- vault.key — Vault가 사용하는 개인 암호화 키
- ca.pem — 노드 간 상호 TLS를 검증하는 데 사용하는 인증 기관(CA)
- 선택: Vault CLI를 설치 하세요.
1단계: 영속적인 Docker 볼륨 만들기
클러스터의 노드 간 Vault와 HAProxy 구성 영속화를 위해 영속적인 Docker 볼륨을 만드는 것을 권장해요.
- Vault 구성 파일용 vault-config 볼륨을 만들어요.
$ docker volume create vault-config
- 프록시 구성 파일용 haproxy-config 볼륨을 만들어요.
$ docker volume create haproxy-config
- 볼륨 생성을 확인해요.
$ docker volume ls | grep 'config'
2단계: 로컬 디렉터리 구조 만들기
Vault 배포 파일과 관련 구성 파일을 보관·버전 관리하려면 로컬 디렉터리 vault-deploy를 만드는 것을 권장해요.
shared-deploy
vault-deploy
|-- local-config
|-- certs
|-- hcl
proxy-deploy
|-- local-config
나머지 단계들은 다음을 저장한다고 가정해요.
- 공유 docker compose 파일은 shared-deploy 아래에.
- Vault용 Docker compose와 Dockerfile은 vault-deploy 아래에.
- Vault 라이선스 파일 vault-license.hclic은 vault-deploy/local-config 아래에.
- 암호화 인증서는 vault-deploy/local-config/certs 아래에.
- Vault 노드 구성 파일은 vault-deploy/local-config/hcl 아래에.
- 로드밸런서용 Docker compose와 Dockerfile은 proxy-deploy 아래에.
- 로드밸런서 구성 파일은 proxy-deploy/local-config 아래에.
3단계: Vault용 Docker 환경 파일 만들기
공통 Vault 환경 변수를 모아 설정하는 단일 파일 vault-deploy/vault.env를 만드는 것을 권장해요.
# vault-config의 마운트 경로
VAULT_CONFIG="/vault/config"
# 라이선스 파일 경로
VAULT_LICENSE_PATH="${VAULT_CONFIG}/vault-license.hclic"
# 노드별 구성 파일을 선택하는 커스텀 변수
NODE_IDX=""
4단계: 네트워크 환경 구성
클러스터 환경을 위한 Docker compose 파일 shared-deploy/cluster-env.yml을 만들어 영속 볼륨과 공유 네트워크를 정의해 구성 파일에서 이름으로 컨테이너·볼륨을 참조할 수 있게 해요.
networks:
<NETWORK_NAME>:
name: <NETWORK_NAME>
driver: bridge
ipam:
config:
- subnet: "<SUBNET>"
ip_range: "<IP_RANGE>"
gateway: "<GATEWAY>"
volumes:
vault-config:
external: true
name: vault-config
haproxy-config:
external: true
name: haproxy-config
예:
networks:
vault_cluster:
name: vault-network
driver: bridge
ipam:
config:
- subnet: "192.168.42.128/25"
ip_range: "192.168.42.128/25"
gateway: "192.168.42.129"
volumes:
vault-config:
external: true
name: vault-config
haproxy-config:
external: true
name: haproxy-config
5단계: Vault용 기본 compose 파일 만들기
Vault 노드의 기본 빌드 프로세스를 정의하는 Docker compose 파일 vault-deploy/vault.compose.yml을 만들어요.
name: <NAME>
services:
<SERVICE_NAME>:
volumes:
- vault-config:/vault/config
env_file:
- path: <ENV_FILE_PATH>
required: true
networks:
- <NETWORK_NAME>
build:
include:
- path: <CLUSTER_ENV_PATH>
예:
name: zcx
services:
vault-base:
volumes:
- vault-config:/vault/config
env_file:
- path: ./vault.env
required: true
networks:
- vault_cluster
build:
include:
- path: ../shared-deploy/cluster-env.yml
6단계: Vault용 기본 Dockerfile 만들기
빌드 파일 vault-deploy/vault.Dockerfile을 만들어 키 파일을 영속 스토리지에 복사하고, Vault 데이터 파일용 디렉터리를 만들고, Vault에 대한 키 디렉터리 소유권·접근 권한을 설정하고, 환경 변수로 관련 노드별 구성 파일로 Vault를 시작하게 해요.
예:
# syntax=docker/dockerfile:1
FROM hashicorp/vault-enterprise:<VERSION>
# config와 license 파일 복사
COPY local-config/hcl/vault-*.hcl /vault/config/hcl/
COPY local-config/vault-license.hclic /vault/config/vault-license.hclic
# cert 파일 복사
COPY local-config/certs/vault.pem /vault/config/certs/vault.pem
COPY local-config/certs/vault.key /vault/config/certs/vault.key
COPY local-config/certs/ca.pem /vault/config/certs/ca.pem
RUN mkdir /vault/data/
RUN mkdir /vault/plugins/
RUN mkdir /vault/plugins/tmp
# Vault가 Vault 관련 파일을 소유하도록 함
RUN chown -R vault:vault /vault
# 인증서 파일의 소유권과 모드 설정
RUN chown root:vault /vault/config/certs/vault-key.pem
RUN chmod 0644 /vault/config/certs/vault-cert.pem
RUN chmod 0644 /vault/config/certs/vault-ca.pem
RUN chmod 0640 /vault/config/certs/vault-key.pem
CMD vault server -config=${VAULT_CONFIG}/hcl/vault-${NODE_IDX}.hcl
7단계: Vault compose 파일 업데이트
- vault.compose.yml 파일의 기본 빌드를 vault.Dockerfile을 사용하도록 업데이트해요.
name: zcx
services:
vault-base:
volumes:
- vault-config:/vault/config
env_file:
- path: ./vault.env
required: true
networks:
- vault_cluster
build:
context: .
dockerfile: vault.Dockerfile
include:
- path: ../shared-deploy/cluster-env.yml
- 기본 빌드 정의를 확장해 클러스터의 각 노드에 대한 빌드 지침을 추가하고, 관련 VAULT_ADDR 환경 변수를 설정하고, NODE_IDX 환경 변수로 vault.Dockerfile의 동작을 커스터마이즈해요. 예:
name: zcx
services:
vault-base:
...
vault-1:
extends:
service: vault-base
container_name: zcx-vault-1
hostname: zcx-vault-1
environment:
VAULT_ADDR: https://zcx-vault-1:8200
NODE_IDX: "1"
vault-2:
extends:
service: vault-base
container_name: zcx-vault-2
hostname: zcx-vault-2
environment:
VAULT_ADDR: https://zcx-vault-2:8200
NODE_IDX: "2"
vault-3:
extends:
service: vault-base
container_name: zcx-vault-3
hostname: zcx-vault-3
environment:
VAULT_ADDR: https://zcx-vault-3:8200
NODE_IDX: "3"
include:
- path: ../shared-deploy/cluster-env.yml
8단계: Vault 구성 파일 만들기
다음 템플릿을 사용해 local-config/hcl/ 아래 각 Vault 노드에 대한 개별 구성 파일(vault-N.hcl)을 만들어요.
ui = true
disable_mlock = true
license_path = "<LICENSE_PATH>"
# --- 루프백이 아닌 인터페이스 구성 ---
api_addr = "https://<NODE_HOSTNAME>:8200"
cluster_addr = "https://<NODE_HOSTNAME>:8201"
cluster_name = "<CLUSTER_NAME>"
# --- 리스너 구성 ---
listener "tcp" {
address = "[::]:8200"
tls_disable = "false"
tls_cert_file = "<CERT_FILE>/vault.pem"
tls_key_file = "<KEY_FILE>/vault.key"
tls_client_ca_file = "<CA_FILE>/ca.pem"
}
# --- 플러그인 구성 ---
plugin_directory = "<PLUGIN_DIR>"
plugin_tmpdir = "<PLUGIN_TMPDIR>"
# --- 통합 스토리지 ---
storage "raft" {
path = "/vault/data"
node_id = "<NODE_ID>"
# 클러스터 노드 N+1용 재조인/리스너 구성
retry_join {
leader_api_addr = "https://<NEXT_NODE>:8200"
leader_client_cert_file = "<CERT_FILE>/vault.pem"
leader_client_key_file = "<KEY_FILE>/vault.key"
leader_ca_cert_file = "<CA_FILE>/ca.pem"
}
# 클러스터 노드 N+2용 재조인/리스너 구성
retry_join {
leader_api_addr = "https://<NEXT_NODE2>:8200"
leader_client_cert_file = "<CERT_FILE>/vault.pem"
leader_client_key_file = "<KEY_FILE>/vault.key"
leader_ca_cert_file = "<CA_FILE>/ca.pem"
}
}
클러스터 내 TLS 통신을 강제하려면 tls_disable을 false로 설정해야 해요. 또한 UI, 라이선스 경로, 플러그인 디렉터리 정보를 설정할 것을 권장해요. 나중에 플러그인 정보를 업데이트하려면 클러스터를 재시작해야 하므로 설정 시점에 플러그인 위치를 정하는 것을 권장합니다.
예:
ui = true
disable_mlock = true
license_path = "/vault/config/vault-license.hclic"
# --- 루프백이 아닌 인터페이스 구성 ---
api_addr = "https://zcx-vault-1:8200"
cluster_addr = "https://zcx-vault-1:8201"
cluster_name = "zcx-vault"
# --- 플러그인 구성 ---
plugin_directory = "/vault/plugins/"
plugin_tmpdir = "/vault/plugins/tmp"
# --- 리스너 구성 ---
listener "tcp" {
address = "[::]:8200"
tls_disable = "false"
tls_cert_file = "/vault/config/certs/vault.pem"
tls_key_file = "/vault/config/certs/vault.key"
tls_client_ca_file = "/vault/config/certs/ca.pem"
}
# --- 통합 스토리지 ---
storage "raft" {
path = "/vault/data"
node_id = "zcx-vault-1"
# 클러스터 노드 N+1용 재조인/리스너 구성
retry_join {
leader_api_addr = "https://zcx-vault-2:8200"
leader_client_cert_file = "/vault/config/certs/vault.pem"
leader_client_key_file = "/vault/config/certs/vault.key"
leader_ca_cert_file = "/vault/config/certs/ca.pem"
}
# 클러스터 노드 N+2용 재조인/리스너 구성
retry_join {
leader_api_addr = "https://zcx-vault-3:8200"
leader_client_cert_file = "/vault/config/certs/vault.pem"
leader_client_key_file = "/vault/config/certs/vault.key"
leader_ca_cert_file = "/vault/config/certs/ca.pem"
}
}
9단계: Vault 클러스터 시작
전체 클러스터를 띄우기 전에 단일 노드를 먼저 띄워 Vault를 초기화하고 루트 토큰을 생성하고 봉인 해제 키를 만들어야 해요.
- Vault 서비스 중 하나를 시작해요. 예를 들어 vault-1:
$ docker compose \
-f vault-deploy/vault.compose.yml up vault-1 --build --detach
- Docker CLI로 컨테이너에 대해
vault operator init을 실행하고 초기화 세부 정보를 안전한 위치의 vault_init.json에 저장해요.
$ docker exec -it zcx-vault-1 \
vault operator init -format=json > /secure/location/vault-init.json
- 생성된 봉인 해제 키로 컨테이너에 대해
vault operator unseal을 실행해요.
$ for token in $(
cat /secure/location/vault-init.json | \
jq -r '.unseal_keys_b64[0:3] | join(" ")'
) ; do
docker exec -it zcx-vault-1 vault operator unseal $token
done
- 컨테이너의 봉인 상태를 확인해요.
$ docker exec -it zcx-vault-1 vault status
--- -----
Seal Type shamir
Initialized true
Sealed false
Total Shares 5
Threshold 3
Version 1.21.2+ent
Build Date 2026-01-06T16:58:57Z
Storage Type raft
Cluster Name zcx-vault
Cluster ID 520515c4-887f-4ace-b267-8625cfd5fb43
Removed From Cluster false
HA Enabled true
HA Cluster https://zcx-vault-1:8201
HA Mode active
Active Since 2026-02-04T02:51:25.540535695Z
Raft Committed Index 9945
Raft Applied Index 9945
Last WAL 3819
- 나머지 Vault 노드에 대해 docker compose와 봉인 해제 단계를 반복해요. (zcx-vault-2, zcx-vault-3)
10단계: HAProxy용 compose 파일 만들기
Vault 클러스터와 같은 네트워크 구성을 사용하는 로드밸런서용 compose 파일 proxy-deploy/proxy.compose.yml을 만들어요. 예:
name: haproxy
services:
vault-lb:
image: ibmz-hc-registry.ngrok.dev/haproxy:3.2
volumes:
- haproxy-config:/usr/local/etc/haproxy
networks:
- vault_cluster
ports:
- "8300:8200"
- "8404:8404"
include:
- path: ../shared-deploy/cluster-env.yml
11단계: HAProxy 구성 파일 만들기
Vault 노드의 컨테이너 이름으로 기본 HAProxy 구성 파일 proxy-deploy/local-config/haproxy.cfg을 만들어요.
다음 예제는 외부 클라이언트를 위한 로드밸런서에서 TLS 종료를 구성하고, Vault 클러스터의 상태 확인을 정의하며, 라운드로빈 분산 전략으로 Vault 피어에 Layer-4 TCP 포워딩을 활성화합니다.
global
maxconn 4096
log stdout format raw local0
defaults
mode tcp # TCP mode for Layer-4 forwarding
timeout connect 5s
timeout client 1m
timeout server 1m
log global
# --- Stats Page (HTTPS) ---
frontend stats
bind *:8404 ssl crt /usr/local/etc/haproxy/haproxy.pem
mode http
stats enable
stats uri /stats
stats refresh 10s
stats admin if TRUE
# --- Vault API Frontend (TLS Passthrough) ---
frontend vault_api
bind *:8200 # Vault API 포트에서 수신
mode tcp # TLS를 보존하기 위한 Layer-4 포워딩
default_backend vault_nodes
# --- Vault Nodes Backend ---
backend vault_nodes
mode tcp
balance roundrobin # 노드 간 단순 부하 분산
option tcp-check # TCP 수준 상태 확인
server node1 zcx-vault-1:8200 check
server node2 zcx-vault-2:8200 check
server node3 zcx-vault-3:8200 check
12단계: 로드밸런서 배포
- Docker CLI로 HAProxy 구성을 영속 볼륨에 복사해요. 예를 들어 임시 컨테이너 haproxy-tmp를 만들고 볼륨에 복사한 뒤 임시 컨테이너를 제거하려면:
$ docker run \
--name haproxy-tmp \
-v haproxy-config:/usr/local/etc/haproxy \
alpine sh -c "sleep 1" \
&& docker cp \
proxy-deploy/local-config/haproxy.cfg \
haproxy-tmp:/usr/local/etc/haproxy/haproxy.cfg \
&& docker rm -f haproxy-tmp
- Docker compose로 HAProxy 로드밸런서를 배포해요.
$ docker compose \
-f proxy-deploy/proxy.compose.yml up vault-lb --build --detach
13단계: 배포 확인
- vault-1 컨테이너의 IP 주소를 가져와요.
$ docker inspect -f \
'{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' zcx-vault-1
- 로컬 VAULT_ADDR 환경 변수를 vault-1 컨테이너 IP로 설정해요.
$ export VAULT_ADDR=https://<IP>:8200
- 로컬 VAULT_TOKEN 환경 변수를 vault-init.json에서 반환된 root_token 값으로 설정해요.
$ export VAULT_TOKEN=hvs.000000000000000000000000
- Vault를 호출해 현재 노드를 나열해요.
CLI:
$ vault operator raft list-peers
Node Address State Voter
---- ------- ----- -----
zcx-vault-1 zcx-vault-1:8201 leader true
zcx-vault-2 zcx-vault-2:8201 follower true
zcx-vault-3 zcx-vault-3:8201 follower true
API:
$ curl \
--request GET \
--header "X-Vault-Token: ${VAULT_TOKEN}" \
${VAULT_ADDR}/v1/sys/storage/raft/configuration | jq .data.config.servers
[
{
"node_id": "zcx-vault-1",
"address": "zcx-vault-1:8201",
"leader": true,
"protocol_version": "3",
"voter": true
},
{
"node_id": "zcx-vault-2",
"address": "zcx-vault-2:8201",
"leader": false,
"protocol_version": "3",
"voter": true
},
{
"node_id": "zcx-vault-3",
"address": "zcx-vault-3:8201",
"leader": false,
"protocol_version": "3",
"voter": true
}
]
- 브라우저에서
https://<proxy_url>:<proxy_port>/stats를 열어 HAProxy 컨테이너 상태를 테스트해요. - 로컬 VAULT_PROXY_ADDR 환경 변수를 로드밸런서의 URL로 설정해요.
$ export VAULT_PROXY_ADDR=https://<PROXY_URL>:<PROXY_PORT>
- proxy URL을 위해 VAULT_ADDR 환경 변수를 해제해요.
$ unset VAULT_ADDR
- Vault를 호출해 현재 스토리지 구성을 확인하고 proxy가 트래픽을 올바르게 라우팅하는지 확인해요.
CLI:
$ vault read -format json sys/storage/raft/configuration | jq .data
{
"config": {
"index": 0,
"servers": [
{
"address": "zcx-vault-1:8201",
"leader": true,
"node_id": "zcx-vault-1",
"protocol_version": "3",
"voter": true
},
{
"address": "zcx-vault-2:8201",
"leader": false,
"node_id": "zcx-vault-2",
"protocol_version": "3",
"voter": true
},
{
"address": "zcx-vault-3:8201",
"leader": false,
"node_id": "zcx-vault-3",
"protocol_version": "3",
"voter": true
}
]
}
}
API:
$ curl \
--request GET \
--header "X-Vault-Token: ${VAULT_TOKEN}" \
${VAULT_PROXY_ADDR}/v1/sys/storage/raft/configuration | jq .data
{
"config": {
"servers": [
{
"node_id": "zcx-vault-1",
"address": "zcx-vault-1:8201",
"leader": true,
"protocol_version": "3",
"voter": true
},
{
"node_id": "zcx-vault-2",
"address": "zcx-vault-2:8201",
"leader": false,
"protocol_version": "3",
"voter": true
},
{
"node_id": "zcx-vault-3",
"address": "zcx-vault-3:8201",
"leader": false,
"protocol_version": "3",
"voter": true
}
],
"index": 0
}
}