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 볼륨을 만드는 것을 권장해요.

  1. Vault 구성 파일용 vault-config 볼륨을 만들어요.
$ docker volume create vault-config
  1. 프록시 구성 파일용 haproxy-config 볼륨을 만들어요.
$ docker volume create haproxy-config
  1. 볼륨 생성을 확인해요.
$ 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 파일 업데이트

  1. 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
  1. 기본 빌드 정의를 확장해 클러스터의 각 노드에 대한 빌드 지침을 추가하고, 관련 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를 초기화하고 루트 토큰을 생성하고 봉인 해제 키를 만들어야 해요.

  1. Vault 서비스 중 하나를 시작해요. 예를 들어 vault-1:
$ docker compose \
  -f vault-deploy/vault.compose.yml up vault-1 --build --detach
  1. Docker CLI로 컨테이너에 대해 vault operator init을 실행하고 초기화 세부 정보를 안전한 위치의 vault_init.json에 저장해요.
$ docker exec -it zcx-vault-1 \
  vault operator init -format=json > /secure/location/vault-init.json
  1. 생성된 봉인 해제 키로 컨테이너에 대해 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
  1. 컨테이너의 봉인 상태를 확인해요.
$ 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
  1. 나머지 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단계: 로드밸런서 배포

  1. 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
  1. Docker compose로 HAProxy 로드밸런서를 배포해요.
$ docker compose \
  -f proxy-deploy/proxy.compose.yml up vault-lb --build --detach

13단계: 배포 확인

  1. vault-1 컨테이너의 IP 주소를 가져와요.
$ docker inspect -f \
  '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' zcx-vault-1
  1. 로컬 VAULT_ADDR 환경 변수를 vault-1 컨테이너 IP로 설정해요.
$ export VAULT_ADDR=https://<IP>:8200
  1. 로컬 VAULT_TOKEN 환경 변수를 vault-init.json에서 반환된 root_token 값으로 설정해요.
$ export VAULT_TOKEN=hvs.000000000000000000000000
  1. 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
    }
  ]
  1. 브라우저에서 https://<proxy_url>:<proxy_port>/stats를 열어 HAProxy 컨테이너 상태를 테스트해요.
  2. 로컬 VAULT_PROXY_ADDR 환경 변수를 로드밸런서의 URL로 설정해요.
$ export VAULT_PROXY_ADDR=https://<PROXY_URL>:<PROXY_PORT>
  1. proxy URL을 위해 VAULT_ADDR 환경 변수를 해제해요.
$ unset VAULT_ADDR
  1. 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
    }
  }

추가 자료

더 알아보기 (Learn more)