Kubernetes – ArgoCD 클러스터에 Vault 추가하기 (Raft 저장소 + AWS S3 백업)

By | 2026년 9월 27일
Table of Contents

Kubernetes – ArgoCD 클러스터에 Vault 추가하기 (Raft 저장소 + AWS S3 백업)

이전 문서에서 만든 EC2 단일 노드 + ArgoCD 클러스터에 HashiCorp Vault 를 추가합니다.

  • 저장소는 Vault 내장 Raft 저장소(Integrated Storage)를 사용합니다. Consul 같은 외부 저장소가 필요 없습니다.
  • 백업은 Raft 스냅샷을 매일 만들어 AWS S3 에 올립니다. S3 업로드는 AWS CLI 로 합니다.
  • 이전 문서와 마찬가지로 설치/변경은 모두 Git push 로 하고, 수동 작업은 최초 초기화(init / unseal)와 비밀값 등록뿐입니다.

Vault OSS 에는 자동 스냅샷 기능(Enterprise 전용)이 없습니다.
그래서 Kubernetes CronJob 으로 vault operator raft snapshot save → aws s3 cp 를 실행합니다.


0. 전체 구조

k8s-gitops-aws/
├── bootstrap/root-app.yaml
├── apps/
│   ├── argocd.yaml
│   ├── nginx.yaml
│   ├── local-path.yaml          # ★ 추가: PVC 를 만들어 줄 StorageClass
│   ├── vault.yaml               # ★ 추가: Vault (Raft)
│   └── vault-backup.yaml        # ★ 추가: S3 백업 CronJob
├── values/
│   ├── argocd/values.yaml
│   ├── nginx/values.yaml
│   ├── local-path/values.yaml   # ★
│   ├── vault/values.yaml        # ★
│   └── vault-backup/values.yaml # ★
└── charts/
    ├── nginx/
    └── vault-backup/            # ★ 자체 차트 (CronJob + ServiceAccount)
        ├── Chart.yaml
        ├── values.yaml
        └── templates/
            ├── serviceaccount.yaml
            └── cronjob.yaml
                    EC2 (단일 노드 Kubernetes)
┌──────────────────────────────────────────────────────────┐
│  namespace: vault                                        │
│  ┌──────────────┐   snapshot   ┌──────────────────────┐  │
│  │ vault-0      │◀─────────────│ CronJob vault-backup │  │
│  │ (Raft)       │  (k8s auth)  │  ① vault (init)      │  │
│  │ PVC 5Gi      │              │  ② aws-cli (upload)  │──┼──▶ S3 버킷
│  └──────┬───────┘              └──────────────────────┘  │     vault/vault-*.snap
│         │ local-path StorageClass                        │     (30일 후 자동 삭제)
│         ▼                                                │
│  /opt/local-path-provisioner (EC2 디스크)                │
└──────────────────────────────────────────────────────────┘
서비스 접속 주소
ArgoCD https://<EC2_PUBLIC_IP>:30443/
nginx http://<EC2_PUBLIC_IP>:31405/
Vault UI http://<EC2_PUBLIC_IP>:30820/

1. 사전 준비

1.1 EC2 사양 / 보안 그룹

Vault 는 메모리를 많이 쓰지 않지만, ArgoCD 와 함께 올리므로 t3.medium(4GB) 이 최소입니다.

보안 그룹 인바운드 규칙 추가

포트 용도 소스
30820 Vault UI 내 IP

이 문서는 구성을 단순하게 하기 위해 Vault 를 TLS 없이(HTTP) 띄웁니다.
30820 포트는 반드시 내 IP 로만 열고, 운영 환경에서는 TLS 를 적용합니다.

1.2 AWS CLI 설치 (로컬 PC)

AmazonS3FullAccess 권한을 가진 IAM 계정을 생성한다.
S3 버킷과 IAM 사용자를 만들 때 사용합니다.

curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o awscliv2.zip
unzip awscliv2.zip
sudo ./aws/install

aws --version
aws configure            # AmazonS3FullAccess 권한을 가진 IAM 계정 Access Key / Region: ap-northeast-2
aws sts get-caller-identity

macOS 는 brew install awscli 로 설치해도 됩니다.


2. S3 버킷 / IAM 사용자 생성 (로컬 PC, AWS CLI)

이하 <BUCKET> 은 전 세계에서 유일한 이름이어야 합니다. 예: <YOUR_ID>-vault-backup

export BUCKET=<YOUR_ID>-vault-backup
export REGION=ap-northeast-2

2.1 버킷 생성

aws s3api create-bucket \
  --bucket $BUCKET \
  --region $REGION \
  --create-bucket-configuration LocationConstraint=$REGION

# 퍼블릭 접근 전부 차단
aws s3api put-public-access-block \
  --bucket $BUCKET \
  --public-access-block-configuration \
  BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true

S3 는 기본적으로 SSE-S3 로 암호화되어 저장됩니다. 또한 Raft 스냅샷 자체도 Vault 의 키로 암호화되어 있습니다.

2.2 오래된 스냅샷 자동 삭제 (Lifecycle)

vault/ 아래 파일을 30일 후 자동 삭제합니다.

lifecycle.json

{
  "Rules": [
    {
      "ID": "expire-vault-snapshots",
      "Filter": { "Prefix": "vault/" },
      "Status": "Enabled",
      "Expiration": { "Days": 30 },
      "AbortIncompleteMultipartUpload": { "DaysAfterInitiation": 1 }
    }
  ]
}
aws s3api put-bucket-lifecycle-configuration \
  --bucket $BUCKET \
  --lifecycle-configuration file://lifecycle.json

aws s3api get-bucket-lifecycle-configuration --bucket $BUCKET

2.3 백업 전용 IAM 사용자

이미 만든 계정의 Access Key 를 사용한다


3. Git 저장소 작성 (로컬 PC)

3.1 StorageClass: local-path-provisioner

kubeadm 클러스터에는 기본 StorageClass 가 없어서, Vault 의 PVC 가 Pending 에 머뭅니다.
EC2 로컬 디스크를 PV 로 쓰게 해 주는 local-path-provisioner 를 먼저 추가합니다.

폴더는 자동으로 생성됩니다.
다만 생성 시점이 설치할 때가 아니라 첫 PVC 가 만들어질 때입니다.

values/local-path/values.yaml

storageClass:
  create: true
  name: local-path
  defaultClass: true          # 기본 StorageClass 로 지정
  reclaimPolicy: Retain       # PVC 를 지워도 데이터 디렉터리는 남김 (Vault 데이터 보호)

nodePathMap:
  - node: DEFAULT_PATH_FOR_NON_LISTED_NODES
    paths:
      - /opt/local-path-provisioner

apps/local-path.yaml

공식 Helm 저장소가 없으므로 GitHub 저장소의 차트 경로를 태그로 고정해서 사용합니다.

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: local-path
  namespace: argocd
spec:
  project: default
  sources:
    - repoURL: https://github.com/rancher/local-path-provisioner.git
      targetRevision: v0.0.37             # 릴리스 태그
      path: deploy/chart/local-path-provisioner
      helm:
        releaseName: local-path
        valueFiles:
          - $values/values/local-path/values.yaml
    - repoURL: https://github.com/<YOUR_ID>/k8s-gitops-aws.git
      targetRevision: main
      ref: values
  destination:
    server: https://kubernetes.default.svc
    namespace: local-path-storage
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

3.2 Vault: values/vault/values.yaml

HA 모드 + Raft 저장소로 설치하되, 단일 노드이므로 replicas: 1 로 둡니다.
나중에 노드를 늘리면 replicas 만 3 으로 바꾸고 vault operator raft join 하면 됩니다.

global:
  enabled: true
  tlsDisable: true

# Agent Injector 는 지금은 사용하지 않음 → 메모리 절약
injector:
  enabled: false

server:
  resources:
    requests:
      memory: 256Mi
      cpu: 100m
    limits:
      memory: 512Mi

  # Raft 데이터 저장 PVC
  dataStorage:
    enabled: true
    size: 5Gi
    storageClass: local-path
    accessMode: ReadWriteOnce

  ha:
    enabled: true
    replicas: 1
    raft:
      enabled: true
      setNodeId: true          # 노드 ID = 파드 이름 (vault-0)
      config: |
        ui = true

        listener "tcp" {
          tls_disable     = 1
          address         = "[::]:8200"
          cluster_address = "[::]:8201"
        }

        storage "raft" {
          path = "/vault/data"
        }

        service_registration "kubernetes" {}

ui:
  enabled: true
  serviceType: NodePort
  serviceNodePort: 30820
  externalPort: 8200

3.3 Vault: apps/vault.yaml

ArgoCD 와 같은 multi-source 방식입니다. 차트 버전은 EC2 에서 확인합니다.

helm repo add hashicorp https://helm.releases.hashicorp.com
helm repo update
helm search repo hashicorp/vault     # CHART VERSION / APP VERSION 확인
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: vault
  namespace: argocd
  # finalizer 없음: Application 을 실수로 지워도 Vault 리소스는 남도록
spec:
  project: default
  sources:
    - repoURL: https://helm.releases.hashicorp.com
      chart: vault
      targetRevision: 0.34.0           # helm search repo 결과로 변경
      helm:
        releaseName: vault
        valueFiles:
          - $values/values/vault/values.yaml
    - repoURL: https://github.com/<YOUR_ID>/k8s-gitops-aws.git
      targetRevision: main
      ref: values
  destination:
    server: https://kubernetes.default.svc
    namespace: vault
  syncPolicy:
    automated:
      prune: false                     # Vault 는 실수로 지워지지 않도록
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

3.4 push

백업 CronJob 은 Vault 초기화가 끝난 뒤(5장) 추가합니다. 우선 여기까지 push 합니다.

git add .
git commit -m "local-path + vault"
git push origin main

4. Vault 초기화 / Unseal (EC2)

4.1 상태 확인

kubectl get storageclass
-------------------------------------------------------------
NAME                   PROVISIONER             RECLAIMPOLICY
local-path (default)   rancher.io/local-path   Retain

kubectl -n vault get pods,pvc

vault-0 은 Running 이지만 READY 0/1 입니다. 아직 초기화(init) 및 봉인 해제(unseal)가 되지 않았기 때문이며, 정상입니다.
이 동안 ArgoCD 에서 vault Application 은 Progressing 으로 표시됩니다.

kubectl -n vault exec -it vault-0 -- vault status
# Initialized  false
# Sealed       true
# Storage Type raft

4.2 초기화

kubectl -n vault exec -it vault-0 -- vault operator init \
  -key-shares=5 -key-threshold=3

출력되는 Unseal Key 5개와 Initial Root Token 을 안전한 곳(비밀번호 관리자 등)에 보관합니다.

⚠️ 이 키를 잃어버리면 Vault 데이터와 S3 백업까지 모두 복구할 수 없습니다.
Raft 스냅샷은 이 키로 암호화되어 있으므로, 키는 백업과 별도의 장소에 보관합니다.

4.3 Unseal

Unseal Key 5개 중 서로 다른 3개를 입력합니다.

kubectl -n vault exec -it vault-0 -- vault operator unseal   # Key 1
kubectl -n vault exec -it vault-0 -- vault operator unseal   # Key 2
kubectl -n vault exec -it vault-0 -- vault operator unseal   # Key 3

kubectl -n vault exec -it vault-0 -- vault status
# Initialized  true
# Sealed       false
# HA Mode      active

vault-0 이 1/1 READY 가 되고, ArgoCD 의 vault 도 Healthy 가 됩니다.

kubectl -n vault exec -it vault-0 -- sh -c \
  'VAULT_TOKEN=<ROOT_TOKEN> vault operator raft list-peers'
--------------------------------------------------------------
Node       Address                        State     Voter
vault-0    vault-0.vault-internal:8201    leader    true

파드가 재시작되면 Vault 는 다시 Sealed 상태가 됩니다. (EC2 재부팅, 노드 재시작 포함)
그때마다 4.3 의 unseal 을 다시 해야 합니다. 자동화하려면 AWS KMS Auto Unseal 을 구성합니다.

4.4 UI 접속

http://<EC2_PUBLIC_IP>:30820/ 에 접속해서 Token 방식으로 Root Token 으로 로그인합니다.


5. S3 백업 구성

5.1 Vault 에 백업용 인증 설정 (EC2)

백업 CronJob 이 Root Token 없이 스냅샷을 만들 수 있도록, Kubernetes 인증과 스냅샷 전용 정책을 만듭니다.
CronJob 파드는 자신의 ServiceAccount 토큰으로 로그인하므로, Vault 토큰을 Secret 에 저장할 필요가 없습니다.

kubectl -n vault exec -it vault-0 -- sh

이하 vault-0 파드 안에서 실행합니다.

export VAULT_TOKEN=<ROOT_TOKEN>

# 1) Kubernetes 인증 활성화
vault auth enable kubernetes
vault write auth/kubernetes/config \
  kubernetes_host="https://kubernetes.default.svc:443"

# 2) 스냅샷 읽기만 가능한 정책
vault policy write raft-snapshot - <<EOF
path "sys/storage/raft/snapshot" {
  capabilities = ["read"]
}
EOF

# 3) vault 네임스페이스의 vault-backup ServiceAccount 에만 정책 부여
vault write auth/kubernetes/role/vault-backup \
  bound_service_account_names=vault-backup \
  bound_service_account_namespaces=vault \
  policies=raft-snapshot \
  ttl=10m

exit

Vault 차트는 기본으로 system:auth-delegator 권한을 Vault 에 부여하므로, 별도의 RBAC 설정 없이 TokenReview 가 동작합니다.

5.2 AWS Access Key 를 Secret 으로 등록 (EC2)

kubectl -n vault create secret generic vault-backup-aws \
  --from-literal=AWS_ACCESS_KEY_ID=<ACCESS_KEY_ID> \
  --from-literal=AWS_SECRET_ACCESS_KEY=<SECRET_ACCESS_KEY>

5.3 백업 차트: charts/vault-backup (로컬 PC)

CronJob 하나에 컨테이너를 두 개 둡니다. 별도 이미지를 빌드하지 않고 공식 이미지만 사용합니다.

  1. initContainer snapshot (hashicorp/vault) : Kubernetes 인증으로 로그인 → 스냅샷을 공유 볼륨에 저장
  2. container upload (amazon/aws-cli) : aws s3 cp 로 S3 업로드

charts/vault-backup/Chart.yaml

apiVersion: v2
name: vault-backup
description: Vault raft snapshot to S3 (aws cli)
type: application
version: 0.1.0
appVersion: "1.0"

charts/vault-backup/values.yaml (차트 기본값)

schedule: "0 3 * * *"
timeZone: "Asia/Seoul"

serviceAccountName: vault-backup

vault:
  image: hashicorp/vault:2.0.3        # Vault 서버와 같은 버전 권장
  addr: http://vault-active.vault.svc:8200
  role: vault-backup

aws:
  image: amazon/aws-cli:latest
  region: ap-northeast-2
  credentialsSecret: vault-backup-aws

s3:
  bucket: ""
  prefix: vault/

charts/vault-backup/templates/serviceaccount.yaml

apiVersion: v1
kind: ServiceAccount
metadata:
  name: {{ .Values.serviceAccountName }}

charts/vault-backup/templates/cronjob.yaml

apiVersion: batch/v1
kind: CronJob
metadata:
  name: {{ .Release.Name }}
spec:
  schedule: {{ .Values.schedule | quote }}
  timeZone: {{ .Values.timeZone | quote }}
  concurrencyPolicy: Forbid
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 3
  jobTemplate:
    spec:
      backoffLimit: 2
      ttlSecondsAfterFinished: 86400
      template:
        spec:
          serviceAccountName: {{ .Values.serviceAccountName }}
          restartPolicy: Never
          volumes:
            - name: snapshot
              emptyDir: {}

          # ① Vault 스냅샷 생성
          initContainers:
            - name: snapshot
              image: {{ .Values.vault.image }}
              env:
                - name: VAULT_ADDR
                  value: {{ .Values.vault.addr | quote }}
              command: ["/bin/sh", "-c"]
              args:
                - |
                  set -e
                  export VAULT_TOKEN=$(vault write -field=token auth/kubernetes/login \
                    role={{ .Values.vault.role }} \
                    jwt=@/var/run/secrets/kubernetes.io/serviceaccount/token)
                  vault operator raft snapshot save /snapshot/vault.snap
                  ls -l /snapshot/vault.snap
              volumeMounts:
                - name: snapshot
                  mountPath: /snapshot

          # ② AWS CLI 로 S3 업로드
          containers:
            - name: upload
              image: {{ .Values.aws.image }}
              envFrom:
                - secretRef:
                    name: {{ .Values.aws.credentialsSecret }}
              env:
                - name: AWS_DEFAULT_REGION
                  value: {{ .Values.aws.region | quote }}
              command: ["/bin/sh", "-c"]
              args:
                - |
                  set -e
                  KEY="{{ .Values.s3.prefix }}vault-$(date -u +%Y%m%dT%H%M%SZ).snap"
                  aws s3 cp /snapshot/vault.snap "s3://{{ required "s3.bucket is required" .Values.s3.bucket }}/${KEY}"
                  echo "uploaded: s3://{{ .Values.s3.bucket }}/${KEY}"
              volumeMounts:
                - name: snapshot
                  mountPath: /snapshot
                  readOnly: true

amazon/aws-cli:latest 대신 특정 버전 태그로 고정하면 더 안전합니다.

5.4 values/vault-backup/values.yaml (이 클러스터의 실제 설정)

s3:
  bucket: <YOUR_ID>-vault-backup

5.5 apps/vault-backup.yaml

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: vault-backup
  namespace: argocd
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  project: default
  sources:
    - repoURL: https://github.com/<YOUR_ID>/k8s-gitops-aws.git
      targetRevision: main
      path: charts/vault-backup
      helm:
        releaseName: vault-backup
        valueFiles:
          - $values/values/vault-backup/values.yaml
    - repoURL: https://github.com/<YOUR_ID>/k8s-gitops-aws.git
      targetRevision: main
      ref: values
  destination:
    server: https://kubernetes.default.svc
    namespace: vault
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

push 전에 결과를 미리 확인합니다.

helm template vault-backup charts/vault-backup -f values/vault-backup/values.yaml
git add .
git commit -m "vault backup to s3"
git push origin main

6. 백업 동작 확인 (EC2)

매일 03:00(KST) 까지 기다리지 않고, CronJob 에서 Job 을 바로 하나 만들어 실행합니다.

kubectl -n vault get cronjob vault-backup

kubectl -n vault create job --from=cronjob/vault-backup vault-backup-manual
kubectl -n vault get pods -w

# 컨테이너별 로그
kubectl -n vault logs job/vault-backup-manual -c snapshot
kubectl -n vault logs job/vault-backup-manual -c upload
# uploaded: s3://<YOUR_ID>-vault-backup/vault/vault-20261013T081500Z.snap

로컬 PC 에서 S3 를 확인합니다.

aws s3 ls s3://$BUCKET/vault/

확인이 끝나면 수동 Job 은 삭제합니다.

kubectl -n vault delete job vault-backup-manual

자주 나는 오류

로그 원인 / 조치
permission denied (snapshot) 5.1 의 role / policy 이름, ServiceAccount 이름 확인
Vault is sealed 4.3 unseal 다시 실행
AccessDenied (upload) IAM 정책의 버킷 이름과 vault/ prefix 확인
Unable to locate credentials (upload) vault-backup-aws Secret 이 vault 네임스페이스에 있는지 확인

7. 복구 (Restore)

7.1 스냅샷 다운로드 (EC2)

sudo apt install -y unzip
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o awscliv2.zip
unzip awscliv2.zip && sudo ./aws/install

aws configure --profile vault-backup     # 2.3 에서 발급한 키, Region: ap-northeast-2

aws s3 ls s3://<BUCKET>/vault/ --profile vault-backup
aws s3 cp s3://<BUCKET>/vault/vault-20261013T081500Z.snap ./restore.snap --profile vault-backup

7.2 스냅샷 복원

같은 Vault 에 되돌리는 경우 (데이터를 실수로 지웠을 때)

kubectl -n vault cp ./restore.snap vault-0:/tmp/restore.snap

kubectl -n vault exec -it vault-0 -- sh -c \
  'VAULT_TOKEN=<ROOT_TOKEN> vault operator raft snapshot restore /tmp/restore.snap'

EC2 를 새로 만든 경우 (클러스터 재구축)

  1. 이전 문서 2장 → 4장, 이 문서 4.1 ~ 4.3 을 진행합니다. 이때 init 으로 생기는 새 키는 임시용입니다.
  2. 새 Root Token 으로 -force 옵션을 붙여 복원합니다.
kubectl -n vault cp ./restore.snap vault-0:/tmp/restore.snap

kubectl -n vault exec -it vault-0 -- sh -c \
  'VAULT_TOKEN=<NEW_ROOT_TOKEN> vault operator raft snapshot restore -force /tmp/restore.snap'
  1. 복원 후에는 원래 클러스터의 Unseal Key / Root Token 이 유효해집니다. Sealed 상태라면 원래 키로 unseal 합니다.
  2. 5.2 의 AWS Secret 을 다시 등록하면 백업 CronJob 도 그대로 동작합니다. (Kubernetes 인증 설정은 스냅샷에 포함되어 함께 복원됩니다.)

-force 는 스냅샷의 키링이 현재 Vault 와 다를 때 필요합니다. 현재 데이터는 모두 덮어써집니다.


8. 운영

Vault 버전 업그레이드

helm search repo hashicorp/vault       # EC2 에서 버전 확인
vi apps/vault.yaml                     # targetRevision 변경
vi charts/vault-backup/values.yaml     # vault.image 도 같은 버전으로
git commit -am "vault upgrade"
git push

StatefulSet 의 업데이트 전략이 OnDelete 이므로, 반영 후 파드를 직접 삭제해야 새 버전이 뜹니다.
재시작 후에는 unseal 을 다시 해야 합니다.

kubectl -n vault delete pod vault-0
# 4.3 unseal 반복

업그레이드 전에는 6장의 방법으로 수동 백업을 먼저 받아 둡니다.

백업 주기 / 보관 기간 변경

  • 주기: values/vault-backup/values.yaml 에 schedule: "0 */6 * * *" 처럼 추가 후 push
  • 보관 기간: 2.2 의 lifecycle.json 에서 Days 를 바꾸고 put-bucket-lifecycle-configuration 재실행

주의사항

  • Unseal Key, Root Token, AWS Access Key 는 Git 에 넣지 않습니다.
  • Unseal Key 를 잃어버리면 S3 백업도 복구할 수 없습니다. 키는 백업과 다른 곳에 보관합니다.
  • 초기 설정이 끝나면 Root Token 은 vault token revoke <ROOT_TOKEN> 으로 폐기하고, 필요할 때 vault operator generate-root 로 다시 만드는 것을 권장합니다.
  • local-path 는 EC2 디스크에 데이터를 저장하므로, EC2 를 종료(terminate)하면 Vault 데이터도 사라집니다. 복구는 S3 백업으로 합니다.

9. 정리

단계 작업 위치
1 보안 그룹 30820 추가, AWS CLI 설치 AWS 콘솔 / 로컬 PC
2 S3 버킷, Lifecycle, 백업용 IAM 사용자 생성 (AWS CLI) 로컬 PC
3 local-path + vault 를 Git 에 추가 후 push 로컬 PC
4 vault operator init / unseal EC2
5 Kubernetes 인증 설정, AWS Secret 등록, 백업 차트 push EC2 / 로컬 PC
6 수동 Job 으로 백업 확인 → aws s3 ls EC2 / 로컬 PC
7 장애 시 aws s3 cp → raft snapshot restore EC2

EC2 를 새로 만들어도 이전 문서 2장 → 4장, 이 문서 4장 → 7.2 만 하면 Vault 데이터까지 S3 에서 복원됩니다.

답글 남기기