apiVersion: v1
kind: ConfigMap
metadata:
name: spring-config
data:
SPRING_PROFILES_ACTIVE: local
EMAIL_SRC: internhasha.official@gmail.com
key:value으로 관리spring-deployment.yamlDeployment 리소스에 spec.template.spec.containers[].envFrom[]에 secretRef 추가ConfigMap 리소스에서 SPRING_MAIL_USERNAME, MANAGEMENT_HEALTH_MAIL_ENABLE 키를 삭제Secret 리소스를 정의, SPRING_MAIL_USERNAME와 SPRING_MAIL_PASSWORD 키를 추가하고, 값을 base64 인코딩하여 작성# deployment
apiVersion: apps/v1 # Kubernetes 리소스 안정화(stable) 버전
kind: Deployment
metadata:
name: spring # deployment 이름
spec:
replicas: 1 # 동시에 실행가능한 pod 개수
selector:
matchLabels:
app: spring # Deployment가 관리할 Pod를 고르는 기준(label)
# pod 템플릿
template:
metadata:
labels:
app: spring # app: spring 라벨(selector.matchLabels와 반드시 일치해야 deployment가 pod 관리 가능)
spec:
# Pod 안에서 돌 컨테이너 목록
containers:
- name: spring
image: endermaru/22-5-team1-server
ports:
- containerPort: 8080
# ConfigMapRef, secretRef
envFrom:
- configMapRef:
name: spring-config
- secretRef:
name: spring-secret
---
# service
apiVersion: v1
kind: Service
metadata:
name: spring # 서비스 이름
spec:
type: LoadBalancer
selector:
app: spring # 어떤 pod에 트래픽을 전달할지 -> app: spring 라벨
ports:
- port: 8080 # service port: 다른 Pod나 외부에서 이 서비스에 접근할 때 사용
targetPort: 8080 # target port: 서비스가 선택된 Pod의 컨테이너로 트래픽을 전달할 포트
---
# ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: spring-config
data:
SPRING_PROFILES_ACTIVE: local
# Health Check 상세 보기
MANAGEMENT_ENDPOINT_HEALTH_SHOW_DETAILS: "always"
---
# Secret
apiVersion: v1
kind: Secret
metadata:
name: spring-secret # secret 이름
type: Opaque # 일반적인 key-value 쌍을 담는 Secret 타입
data: # stringData 속성이라면 값을 인코딩처리하지 않아도 됨
SPRING_MAIL_USERNAME: aW50ZXJuaGFzaGEuZGV2QGdtYWlsLmNvbQ== # base64 인코딩된 값
SPRING_MAIL_PASSWORD: ...
$ kubectl apply -f spring-deployment.yaml
deployment.apps/spring configured
service/spring unchanged
configmap/spring-config configured
secret/spring-secret created
# secret 적용으로 인한 pod 재시작
$ kubectl rollout restart deploy spring
minikube service spring & /actuator/health로 접속{
"status": "UP",
"groups": [
"liveness",
"readiness",
"startup"
],
"components": {
"db": {
"status": "UP",
"details": {
"database": "H2",
"validationQuery": "isValid()"
}
},
"diskSpace": {
"status": "UP",
"details": {
"total": 1081101176832,
"free": 1019465510912,
"threshold": 10485760,
"path": "/app/.",
"exists": true
}
},
"livenessState": {
"status": "UP"
},
"mail": {
"status": "UP",
"details": {
"location": "smtp.gmail.com:587"
}
},
"ping": {
"status": "UP"
},
"readinessState": {
"status": "UP"
},
"redis": {
"status": "UP",
"details": {
"version": "7.0.15"
}
},
"ssl": {
"status": "UP",
"details": {
"validChains": [],
"invalidChains": []
}
}
}
}
Helm
- Kubernetes에서 애플리케이션을 패키징하고 배포할 수 있게 해주는 패키지 관리자
- 복잡한 Kubernetes 리소스들 (
Deployment,Service,ConfigMap,Secret등)을 하나의 템플릿 세트로 묶어서 관리Chart
- Helm에서 쓰이는 패키지 단위
Chart= Kubernetes 리소스 템플릿 묶음- ex)
vaultChart
vault용Deploymentvault용Service- 설정 값 파일 (
values.yaml)HashiCorp Vault
- k8s에서 Vault를 쉽게 배포하고 관리할 수 있게 해주는 공식 Helm 패키지
Vault: 민감한 데이터를 안전하게 저장하고 관리하는 오픈소스 도구- API 키, 자격 증명, 토큰 등의 시크릿을 중앙 집중식으로 관리
- 암호화 서비스와 신원 기반 접근 제어를 제공
참고자료: https://sseokseok.tistory.com/7
$ helm version
version.BuildInfo{Version:"v3.17.3", GitCommit:"e4da49785aa6e6ee2b86efd5dd9e43400318262b", GitTreeState:"clean", GoVersion:"go1.23.7"}
$ helm repo add hashicorp https://helm.releases.hashicorp.com
# 설정된 모든 Helm 저장소에서 최신 Chart 정보를 가져옴 & 로컬 캐시 새로고침
$ helm repo update
server.dev.enabled): 메모리 기반, 고정된 root 토큰# helm install <사용자 지정 release 이름> <설치할 chart 이름> <args>
$ helm install vault hashicorp/vault --set "server.dev.enabled=true"
# 배포 확인
# vault-0: 실제 Vault 서버가 실행
# vault-agent-injector: Pod 생성 시 특정 조건을 만족하면(vault.hashicorp.com/agent-inject: 'true')
# 자동으로 Vault Agent를 주입(해당 Pod이 Vault 시크릿을 가져올 수 있는 기능)
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
redis-5dd6f67ff-kvt82 1/1 Running 0 5h40m
spring-bb7bddb6c-7jl5g 1/1 Running 0 16m
vault-0 1/1 Running 0 51s
vault-agent-injector-75f9dfc9c8-fnxrt 1/1 Running 0 52s
# vault-0 Pod 내부로 접속, shell 실행
$ kubectl exec -it vault-0 -- //bin//sh
# 상태 확인
/ $ vault status
Key Value
--- -----
Seal Type shamir
Initialized true
Sealed false # Unseal된 상태 (사용 가능 상태)
Total Shares 1
Threshold 1
Version 1.19.0
Build Date 2025-03-04T12:36:40Z
Storage Type inmem # 메모리 기반 저장
Cluster Name vault-cluster-ce82c362
Cluster ID a03c423a-41ff-20c4-a1bc-d0bf995ff845
HA Enabled false # 고가용성(여러 서버, 무중단) 비활성화
# Vault Cli에서 계속 진행
# vault kv put secret/<경로> <키>:<값>
/ $ vault kv put secret/myapp SPRING_PASSWORD=supersecret
== Secret Path ==
secret/data/myapp # 저장위치
======= Metadata =======
Key Value
--- -----
created_time 2025-04-22T14:28:41.092615885Z
custom_metadata <nil>
deletion_time n/a
destroyed false
version 1
# 한 번에 여러 쌍 저장
vault kv put secret/myapp \
SPRING_REDIS_HOST=redis \
SPRING_REDIS_PORT=6379 \
SPRING_DATASOURCE_PASSWORD=supersecret \
SPRING_JWT_SECRET=very-secret-key
vault kv put + .env 파일 파싱.env
SPRING_MAIL_USERNAME=internhasha.dev@gmail.com
SPRING_MAIL_PASSWORD=...
# pod 바깥의 bash 터미널, env 파일을 pod 안으로 복사
$ kubectl cp ./.env vault-0:/tmp/.env
# 다시 pod 안으로 접속
$ kubectl exec -it vault-0 -- //bin//sh
# env 등록
args=""
while IFS='=' read -r key value; do
if [ -z "$key" ] || [ -z "$value" ] || echo "$key" | grep -q '^#'; then
continue
fi
args="$args \"$key=$value\""
done < /tmp/.env
eval vault kv put secret/myapp $args
# 키 값 확인
/ $ vault kv get secret/myapp
== Secret Path ==
secret/data/myapp
======= Metadata =======
Key Value
--- -----
created_time 2025-04-22T15:39:34.866360453Z
custom_metadata <nil>
deletion_time n/a
destroyed false
version 2
============== Data ==============
Key Value
--- -----
SPRING_MAIL_PASSWORD ...
SPRING_MAIL_USERNAME internhasha.dev@gmail.com
# 조회: vault kv get -field=<키> secret/<경로>
/ $ vault kv get -field=SPRING_MAIL_USERNAME secret/myapp
internhasha.dev@gmail.com
# 수정: vault kv patch secret/myapp <키>=<새로운 값>
/ $ vault kv patch secret/myapp SPRING_MAIL_USERNAME=noname
== Secret Path ==
secret/data/myapp
======= Metadata =======
Key Value
--- -----
created_time 2025-06-24T13:50:56.981776588Z
custom_metadata <nil>
deletion_time n/a
destroyed false
version 3 # 수정되며 버전이 올라감
# Vault에서는 "키 단위 삭제"는 직접 지원하지 않고,
# patch 명령으로 특정 키를 덮어쓰기하거나,
# 전체 경로 삭제로 간접적으로 처리
/ $ vault kv delete secret/myapp # 버전 삭제 (soft)
/ $ vault kv metadata delete secret/myapp # 전체 경로 및 메타데이터 삭제
# 특정 버전 조회 -> 앞서 먼저 저장한 SPRING_PASSWORD만 조회
/ $ vault kv get -version=1 secret/myapp
...
========= Data =========
Key Value
--- -----
SPRING_PASSWORD supersecret
Vault가 "누가 K8s 안에서 Secret을 요청했는지"를 판단해서 허용하거나 거절
ServiceAccount: Pod가 클러스터 리소스에 접근할 수 있도록 인증 정보를 제공하는 리소스spring-deployment.yamlserviceAccount 리소스 추가# deployment, service, configmap 생략
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: spring
namespace: default
# Vault Pod 내부에서 실행
$ kubectl exec -it vault-0 -- //bin//sh
# Vault에 Kubernetes Auth 활성화
/ $ vault auth enable kubernetes
Success! Enabled kubernetes auth method at: kubernetes/
# Vault에 k8s 클러스터 정보를 등록
# 사용할 JWT 토큰+k8s API 주소+k8s 클러스터의 인증서
/ $ vault write auth/kubernetes/config \
token_reviewer_jwt="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \
kubernetes_host="https://${KUBERNETES_PORT_443_TCP_ADDR}:443" \
kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
# Vault policy 생성, 등록
# 임시 파일에 secret/data/myapp의 읽기 권한을 담은 정책을 저장
/ $ cat <<EOF > /tmp/spring-policy.hcl
path "secret/data/myapp" {
capabilities = ["read"]
}
EOF
# 정책 등록(spring-app은 정책 이름 = 식별자)
/$ vault policy write spring-app /tmp/spring-policy.hcl
# Role 생성: Vault에 “이 ServiceAccount는 해당 Policy로 접근 가능"하다고 정의
# role의 spring-app: Vault에 등록되는 Role 이름
# default 네임스페이스의 Spring이라는 이름의 ServiceAccount만 Role 사용 가능
# 이전에 등록한 spring-app 정책 사용, 24시간 뒤 토큰 만료
/ $ vault write auth/kubernetes/role/spring-app \
bound_service_account_names=spring \
bound_service_account_namespaces=default \
policies=spring-app \
ttl=2
Success! Data written to: auth/kubernetes/role/spring-app4h
Spring Pod(ServiceAccount: spring)
│
▼
Vault(k8s auth role: spring-app) → policy: spring-app → 권한 부여 → secret/data/myapp
ServiceAccount를 가지고 Vault에 저장된 Secret 값을 k8s Secret으로 변환, Pod에 주입# Vault 바깥의 터미널에서 ESO helm chart 설치
$ helm repo add external-secrets https://charts.external-secrets.io
$ helm repo update
# 클러스터의 external-secrets-system 네임스페이스에 설치
$ helm install external-secrets \
external-secrets/external-secrets \
-n external-secrets-system \
--create-namespace \
--set installCRDs=true
# CRD(Custom Resource Definition): k8s에 새로운 리소스 타입을 추가하는 기능
# ExternalSecret 등의 리소스 타입 정의에 필요
# ESO pod 확인
$ kubectl get pods -n external-secrets-system
NAME READY STATUS RESTARTS AGE
external-secrets-bfb7c98c4-4z482 1/1 Running 0 31s
external-secrets-cert-controller-c794f6dd5-4s474 1/1 Running 0 31s
external-secrets-webhook-6df969d65c-lfr8g 1/1 Running 0 31s
spring-deployment.yamlClusterSecretStore: "Vault에 어떻게 접근할지"를 정의하는 설정, Cluster 레벨 리소스ExternalSecret: Vault의 어떤 값을 Kubernetes의 Secret으로 변환할지 설정하는 리소스Deployment에는 기존에 사용하던 Secret 리소스는 삭제# Deployment, Service, ConfigMap은 그대로, Secret은 삭제
...
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: spring
namespace: default
---
# Vault에 접근하기 위한 정보, 연결방법 정의
apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata:
name: vault-store
spec:
provider:
vault:
server: "http://vault.default.svc.cluster.local:8200" # vault의 주소
path: "secret" # vault의 secret 저장 위치
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "spring-app" # Vault에 등록한 Role
serviceAccountRef: # Vault 인증에 사용할 K8s 서비스 어카운트
name: spring
namespace: default
---
# vault-store를 통해 vault에 접근해 저장된 secret을
# spring-secret 이름의 k8s Secret 리소스로 변환하도록 설정
# (실제 기능은 external-secret-system 네임스페이스의 ESO pod이 수행)
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: spring-secrets
namespace: default
spec:
refreshInterval: 1h # 1시간마다 최신 값 확인
secretStoreRef:
name: vault-store # 참조할 ClusterSecretStore
kind: ClusterSecretStore
target:
name: spring-secret # spring-secret을 대체
creationPolicy: Owner
dataFrom: # 모든 키-값을 한 번에 K8s Secret으로 가져오기
- extract:
key: myapp
# 키를 지정해 가져오기
# data:
# - secretKey: SPRING_MAIL_USERNAME # k8s Secret에서 사용할 키 이름
# remoteRef:
# key: myapp # vault 경로
# property: SPRING_MAIL_USERNAME # Vault 키
# - secretKey: SPRING_MAIL_PASSWORD
# remoteRef:
# key: myapp
# property: SPRING_MAIL_PASSWORD
$ kubectl apply -f spring-deployment.yaml
deployment.apps/spring unchanged
service/spring unchanged
configmap/spring-config unchanged
serviceaccount/spring unchanged
clustersecretstore.external-secrets.io/vault-store created
externalsecret.external-secrets.io/spring-secrets created
# 확인
$ kubectl get pods -A
NAMESPACE NAME READY STATUS RESTARTS AGE
default redis-5dd6f67ff-kvt82 1/1 Running 0 9h
default spring-6b684dd99f-bqn27 1/1 Running 0 21m
default vault-0 1/1 Running 0 3h44m
default vault-agent-injector-75f9dfc9c8-fnxrt 1/1 Running 0 3h44m
external-secrets-system external-secrets-bfb7c98c4-4z482 1/1 Running 0 8m47s
external-secrets-system external-secrets-cert-controller-c794f6dd5-4s474 1/1 Running 0 8m47s
external-secrets-system external-secrets-webhook-6df969d65c-lfr8g 1/1 Running 0 8m47s
...
minikube service spring & /actuator/health로 접속UP으로 표시됨{
"status": "UP",
"groups": [
"liveness",
"readiness",
"startup"
],
"components": {
"db": {
"status": "UP",
"details": {
"database": "H2",
"validationQuery": "isValid()"
}
},
"diskSpace": {
"status": "UP",
"details": {
"total": 1081101176832,
"free": 1018622791680,
"threshold": 10485760,
"path": "/app/.",
"exists": true
}
},
"livenessState": {
"status": "UP"
},
"mail": {
"status": "UP",
"details": {
"location": "smtp.gmail.com:587"
}
},
"ping": {
"status": "UP"
},
"readinessState": {
"status": "UP"
},
"redis": {
"status": "UP",
"details": {
"version": "7.0.15"
}
},
"ssl": {
"status": "UP",
"details": {
"validChains": [],
"invalidChains": []
}
}
}
}
spring-deployment.yaml 파일에 정의된 모든 리소스를 한 번에 삭제:# 모든 리소스 확인
$ kubectl get all -A
# manifest에 정의된 리소스 삭제
$ kubectl delete -f spring-deployment.yaml
# Vault 삭제
helm uninstall vault
# External Secrets Operator 삭제
helm uninstall external-secrets -n external-secrets-system
# ESO 네임스페이스도 삭제
kubectl delete namespace external-secrets-system
minikube stop
ClusterSecretStore, ExternalSecret)Hashicorp Vault pod을 클러스터에 설치, 값 설정ServiceAccount 리소스 정의 & Deployment에서 추가 & Vault에 해당 리소스에 대한 정책 추가External Secrets Operator pod을 클러스터에 설치ClusterSecretStore, 특정 secret을 가져와 spring-secret으로 변환하는 설정을 담은 ExternalSecret 리소스를 추가ExternalSecret 감지 + ClusterSecretStore에서 spring이라는 ServiceAccount 토큰을 가지고 Vault 접근secret/data/myapp의 secret 접근 허용ExternalSecret 설정에 따라 ESO 컨트롤러에 의해서 spring-secret이라는 Secret 리소스로 변환Deployment의 secretRef에 의해 spring-secret의 값들이 pod의 환경변수로 주입됨┌──────────────────┐ ┌────────────────────┐ ┌───────────────────┐
│ HashiCorp │ │ External │ │ Kubernetes │
│ Vault │◀───│ Secrets │───▶│ Secret │
│ (실제 데이터) │ │ Operator │ │ (spring-secret) │
└──────────────────┘ └────────────────────┘ └───────────────────┘
▲ ▲ ▲
│ │ │
vault kv put ClusterSecretStore Pod에서 사용
secret/myapp + ExternalSecret
spring-deployment.yaml 전체 파일# deployment
apiVersion: apps/v1 # Kubernetes 리소스 안정화(stable) 버전
kind: Deployment
metadata:
name: spring # deployment 이름
spec:
replicas: 1 # 동시에 실행가능한 pod 개수
selector:
matchLabels:
app: spring # Deployment가 관리할 Pod를 고르는 기준(label)
# pod 템플릿
template:
metadata:
labels:
app: spring # app: spring 라벨(selector.matchLabels와 반드시 일치해야 deployment가 pod 관리 가능)
spec:
# Pod 안에서 돌 컨테이너 목록
containers:
- name: spring
image: endermaru/22-5-team1-server
ports:
- containerPort: 8080
# ConfigMapRef, secretRef
envFrom:
- configMapRef:
name: spring-config
- secretRef:
name: spring-secret
---
# service
apiVersion: v1
kind: Service
metadata:
name: spring # 서비스 이름
spec:
type: LoadBalancer
selector:
app: spring # 어떤 pod에 트래픽을 전달할지 -> app: spring 라벨
ports:
- port: 8080 # service port: 다른 Pod나 외부에서 이 서비스에 접근할 때 사용
targetPort: 8080 # target port: 서비스가 선택된 Pod의 컨테이너로 트래픽을 전달할 포트
---
# ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: spring-config
data:
SPRING_PROFILES_ACTIVE: local
# Health Check 상세 보기
MANAGEMENT_ENDPOINT_HEALTH_SHOW_DETAILS: "always"
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: spring
namespace: default
---
# Vault에 접근하기 위한 정보, 연결방법 정의
apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata:
name: vault-store
spec:
provider:
vault:
server: "http://vault.default.svc.cluster.local:8200" # vault의 주소
path: "secret" # vault의 secret 저장 위치
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "spring-app" # Vault에 등록한 Role
serviceAccountRef: # Vault 인증에 사용할 K8s 서비스 어카운트
name: spring
namespace: default
---
# vault-store를 통해 vault에 접근해 저장된 secret을
# spring-secret 이름의 k8s Secret 리소스로 변환하도록 설정
# (실제 기능은 external-secret-system 네임스페이스의 ESO pod이 수행)
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: spring-secrets
namespace: default
spec:
refreshInterval: 1h # 1시간마다 최신 값 확인
secretStoreRef:
name: vault-store # 참조할 ClusterSecretStore
kind: ClusterSecretStore
target:
name: spring-secret # spring-secret을 대체
creationPolicy: Owner
dataFrom: # 모든 키-값을 한 번에 K8s Secret으로 가져오기
- extract:
key: myapp
# 키를 지정해 가져오기
# data:
# - secretKey: SPRING_MAIL_USERNAME # k8s Secret에서 사용할 키 이름
# remoteRef:
# key: myapp # vault 경로
# property: SPRING_MAIL_USERNAME # Vault 키
# - secretKey: SPRING_MAIL_PASSWORD
# remoteRef:
# key: myapp
# property: SPRING_MAIL_PASSWORD