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 하나에 컨테이너를 두 개 둡니다. 별도 이미지를 빌드하지 않고 공식 이미지만 사용합니다.
- initContainer
snapshot(hashicorp/vault) : Kubernetes 인증으로 로그인 → 스냅샷을 공유 볼륨에 저장 - 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 를 새로 만든 경우 (클러스터 재구축)
- 이전 문서 2장 → 4장, 이 문서 4.1 ~ 4.3 을 진행합니다. 이때 init 으로 생기는 새 키는 임시용입니다.
- 새 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'
- 복원 후에는 원래 클러스터의 Unseal Key / Root Token 이 유효해집니다. Sealed 상태라면 원래 키로 unseal 합니다.
- 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 에서 복원됩니다.