이 글은 CloudNet@팀의 AWS EKS Workshop Study(AEWS) 3기 스터디 내용을 바탕으로 작성되었습니다.
AEWS는 CloudNet@의 '가시다'님께서 진행하는 스터디로, EKS를 학습하는 과정입니다.
EKS를 깊이 있게 이해할 기회를 주시고, 소중한 지식을 나눠주시는 가시다님께 다시 한번 감사드립니다.
이 글이 EKS를 학습하는 분들께 도움이 되길 바랍니다.
본 실습은 Windows WSL2 환경에서 Jenkins를 컨테이너 기반으로 진행합니다. Jenkins는 도커 네트워크(kind) 위에서 구동되며, 호스트 포트 포워딩(8080)을 통해 브라우저에서 접근이 가능합니다.
WSL2를 사용하는 Windows 환경에서는 IP 주소 설정이 필요하므로, 아래 절차를 반드시 따라야 합니다.
# 클러스터 배포 전 Docker가 실행 중인지 확인
docker ps
# 실습 디렉터리 생성
mkdir cicd-labs
cd ~/cicd-labs
# WSL2 Ubuntu에서 사용하는 eth0 IP 확인
ifconfig eth0
# 본인의 IP를 변수로 설정
MyIP=<각자 자신의 WSL2 Ubuntu eth0 IP>
MyIP=172.19.21.65
# cicd-labs 디렉터리에서 아래 kind 클러스터 구성 YAML 파일 작성
cat > kind-3node.yaml <<EOF
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
networking:
apiServerAddress: "$MyIP"
nodes:
- role: control-plane
extraPortMappings:
- containerPort: 30000
hostPort: 30000
- containerPort: 30001
hostPort: 30001
- containerPort: 30002
hostPort: 30002
- containerPort: 30003
hostPort: 30003
- containerPort: 30004
hostPort: 30004
- containerPort: 30005
hostPort: 30006
- role: worker
- role: worker
EOF
# Kind 클러스터 배포
kind create cluster --config kind-3node.yaml --name myk8s --image kindest/node:v1.32.2
# 클러스터 상태 확인
kind get nodes --name myk8s
kubens default



# kind 는 별도 도커 네트워크 생성 후 사용 : 기본값 172.18.0.0/16
docker network ls
docker inspect kind | jq
# k8s api 주소 확인 : 어떻게 로컬에서 접속이 되는 걸까?
kubectl cluster-info
# 노드 정보 확인 : CRI 는 containerd 사용
kubectl get node -o wide
# 파드 정보 확인 : CNI 는 kindnet 사용
kubectl get pod -A -o wide
# 네임스페이스 확인 >> 도커 컨테이너에서 배운 네임스페이스와 다릅니다!
kubectl get namespaces
# 컨트롤플레인/워커 노드(컨테이너) 확인 : 도커 컨테이너 이름은 myk8s-control-plane , myk8s-worker/worker-2 임을 확인
docker ps
docker images
# 디버그용 내용 출력에 ~/.kube/config 권한 인증 로드
kubectl get pod -v6
# kube config 파일 확인 : "server: https://172.19.21.65:35413" 부분에 접속 주소 잘 확인해두자!
cat ~/.kube/config
ls -l ~/.kube/config



Jenkins 컨테이너는 Kind 네트워크를 사용해야 하므로, 먼저 Kind 클러스터를 구성한 후 해당 네트워크가 생성되어 있어야 합니다.
# 작업 디렉토리 생성 후 이동
cd cicd-labs
# cicd-labs 작업 디렉토리 IDE(VSCODE 등)로 열어두기
# kind 설치를 먼저 진행하여 docker network(kind) 생성 후 아래 Jenkins,gogs 생성 할 것
# docker network 확인 : kind 를 사용
docker network ls
...
7e8925d46acb kind bridge local
...
# docker-compose.yaml 작성
# jenkins_home 볼륨은 Jenkins 데이터를 유지하기 위한 볼륨. kind 네트워크는 external로 지정
cat <<EOT > docker-compose.yaml
services:
jenkins:
container_name: jenkins
image: jenkins/jenkins
restart: unless-stopped
networks:
- kind
ports:
- "8080:8080"
- "50000:50000"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- jenkins_home:/var/jenkins_home
volumes:
jenkins_home:
networks:
kind:
external: true
EOT
# 배포
docker compose up -d
docker compose ps
docker inspect kind
# 기본 정보 확인
for i in jenkins ; do echo ">> container : $i <<"; docker compose exec $i sh -c "whoami && pwd"; echo; done
# 도커를 이용하여 각 컨테이너로 접속
docker compose exec jenkins bash
exit




# Jenkins 초기 암호 확인
docker compose exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword
09a21116f3ce4f27a0ede79372febfb1
# Jenkins 웹 접속 주소 확인 : 계정 / 암호 입력 >> admin / qwe123
웹 브라우저에서 http://127.0.0.1:8080 접속 # Windows
# (참고) 로그 확인 : 플러그인 설치 과정 확인
docker compose logs jenkins -f






Jenkins를 외부(브라우저)에서 접속하기 위해, WSL2의 eth0 IP 주소를 기반으로 설정합니다.
# WSL2 Ubuntu IP 확인
ifconfig eth0
# 예시 결과:
# inet 172.19.21.65 netmask 255.255.240.0 broadcast 172.19.31.255
브라우저 주소창에 입력할 URL: http://<WSL2 eth0 IP>:8080
ex) http://172.19.21.65:8080




ArgoCD는 Kubernetes에서 선언형 GitOps 방식으로 애플리케이션을 관리할 수 있는 도구입니다. 본 실습에서는 Helm Chart를 사용하여 ArgoCD를 설치하고, 웹 UI에 접속하여 초기 설정을 수행합니다.
# 작업 디렉터리로 이동
cd cicd-labs
# 네임스페이스 생성 및 Helm 파라미터 파일 작성
kubectl create ns argocd
cat <<EOF > argocd-values.yaml
dex:
enabled: false
server:
service:
type: NodePort
nodePortHttps: 30002
extraArgs:
- --insecure # HTTPS 대신 HTTP 사용
EOF
dex.enabled: false: 인증 서버 비활성화 설정--insecure: HTTPS 대신 HTTP로 접속을 허용하는 설정
# 설치
helm repo add argo https://argoproj.github.io/argo-helm
helm install argocd argo/argo-cd \
--version 7.8.13 \
-f argocd-values.yaml \
--namespace argocd

# 확인
kubectl get pod,svc,ep,secret,cm -n argocd
kubectl get crd | grep argo
# 출력 예시
applications.argoproj.io 2024-04-14T08:12:16Z
applicationsets.argoproj.io 2024-04-14T08:12:17Z
appprojects.argoproj.io 2024-04-14T08:12:16Z

kubectl get appproject -n argocd -o yaml
# configmap
kubectl get cm -n argocd argocd-cm -o yaml
kubectl get cm -n argocd argocd-rbac-cm -o yaml
...
data:
policy.csv: ""
policy.default: ""
policy.matchMode: glob
scopes: '[groups]'
policy.default와 policy.csv가 비어 있다면, 기본 권한은 Argo CD 내장 계정 기준으로 동작하게 됩니다.

Argo CD 설치 시 자동 생성된 관리자 계정(admin)의 초기 암호는 Kubernetes Secret에 저장되어 있습니다.
# 최초 접속 암호 확인
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d ;echo
# 출력 예시
XxJMMJUv8MHZa-kk

Argo CD 서버는 NodePort(기본값: 30002)로 서비스됩니다.
http://127.0.0.1:30002브라우저에서 접속한 후, 초기 admin 계정과 위에서 확인한 암호로 로그인합니다.

로그인 후 상단 우측의 User Info 메뉴에서 UPDATE PASSWORD 항목을 클릭하여 초기 비밀번호를 변경합니다. (ex. qwe12345)

Settings 탭을 통해 다음 항목들을 확인할 수 있습니다.





HashiCorp Vault Plugin은 Jenkins에서 HashiCorp Vault를 활용하여 비밀 정보를 안전하게 주고받을 수 있도록 해주는 플러그인입니다. 이 플러그인을 설치하면 Jenkins 파이프라인이나 빌드 작업에서 Vault의 시크릿 데이터를 연동하여 사용할 수 있습니다.




HashiCorp Vault는 신원 기반(identity-based)의 시크릿 및 암호화 관리 시스템입니다. 이 시스템은 인증(authentication) 및 인가(authorization) 방법을 통해 암호화 서비스를 제공하여 비밀에 대한 안전하고 감사 가능하며 제한된 접근을 보장합니다.
시크릿(Secret)이란 접근을 철저히 통제하고자 하는 모든 것을 의미하며, 예를 들어 토큰, API 키, 비밀번호, 암호화 키 또는 인증서 등이 이에 해당합니다. Vault는 모든 시크릿에 대해 통합된 인터페이스를 제공하면서, 엄격한 접근 제어와 상세한 감사 로그 기록 기능을 제공합니다.
외부 서비스용 API 키, 서비스 지향 아키텍처 간 통신을 위한 자격 증명 등은 플랫폼에 따라 누가 어떤 비밀에 접근했는지를 파악하기 어려울 수 있습니다. 여기에 키 롤링(교체), 안전한 저장, 상세한 감사 로그까지 추가하려면 별도의 커스텀 솔루션 없이는 거의 불가능합니다. Vault는 바로 이 지점에서 해결책을 제공합니다.
Vault는 클라이언트(사용자, 기계, 애플리케이션 등)를 검증하고 인가한 후에만 비밀이나 저장된 민감한 데이터에 접근할 수 있도록 합니다.

Vault는 주로 토큰(Token)을 기반으로 작동하며, 이 토큰은 클라이언트의 정책(Policy)과 연결되어 있습니다. 각 정책은 경로(path) 기반으로 설정되며, 정책 규칙은 클라이언트가 해당 경로에서 수행할 수 있는 작업과 접근 가능성을 제한합니다.
Vault에서는 토큰을 수동으로 생성해 클라이언트에 할당할 수도 있고, 클라이언트가 로그인하여 토큰을 직접 획득할 수도 있습니다.

Vault의 핵심 워크플로우는 다음 네 단계로 구성됩니다:
호텔 체크인 절차에 비유한 Vault 동작 방식

왜 Vault가 필요한가요?
오늘날 대부분의 기업은 자격 증명이 조직 전반에 걸쳐 무분별하게 퍼져 있습니다. → 시크릿 스프롤(Sprwal)
비밀번호, API 키, 자격 증명 등이 일반 텍스트로 앱 소스 코드, 설정 파일, 기타 여러 위치에 저장되어 있습니다. 자격 증명이 이처럼 여기저기 흩어져 있으면 누가 무엇에 접근하고 권한이 있는지를 명확히 파악하기 어렵고, 그로 인해 큰 부담이 따릅니다. 일반 텍스트로 자격 증명을 저장하면 내부 공격자든 외부 공격자든 악의적인 공격 가능성이 크게 증가합니다.
Vault는 이러한 문제를 해결하기 위해 설계되었습니다.
Vault는 이러한 모든 자격 증명을 한 곳에 중앙 집중화하여 정의함으로써, 자격 증명의 불필요한 노출을 줄입니다. 하지만 Vault는 여기서 멈추지 않고, 사용자, 애플리케이션, 시스템이 인증 및 명시적으로 인가된 후에만 리소스에 접근할 수 있도록 보장하며, 클라이언트의 모든 작업 기록을 추적하고 저장하는 감사 로그 기능도 제공합니다.
Vault의 주요 기능은 다음과 같습니다:
안전한 비밀 저장 (Secure Secret Storage): → Static 시크릿
Vault는 임의의 key/value 형식의 시크릿을 저장할 수 있으며, 이 시크릿은 영구 저장소에 기록되기 전에 암호화됩니다. 따라서 저장소에 직접 접근하더라도 비밀을 열람할 수 없습니다.
Vault는 Disk, Consul 등 다양한 저장소를 지원합니다.
동적 비밀 (Dynamic Secrets):
Vault는 AWS나 SQL 데이터베이스와 같은 일부 시스템에 대해 요청 시 비밀을 동적으로 생성할 수 있습니다.
예를 들어, 애플리케이션이 S3 버킷에 접근해야 할 때 Vault에 자격 증명을 요청하면, Vault는 해당 권한을 가진 AWS 키쌍을 생성해줍니다. 이 동적 시크릿은 일정 시간이 지나면 자동으로 폐기됩니다.
데이터 암호화 (Data Encryption):
Vault는 데이터를 저장하지 않고 암호화 및 복호화를 수행할 수 있습니다.
이를 통해 보안 팀은 암호화 매개변수를 정의하고, 개발자는 암호화된 데이터를 SQL 데이터베이스 등 외부 저장소에 안전하게 저장할 수 있습니다.
임대 및 갱신 (Leasing and Renewal):
Vault에 저장된 모든 시크릿은 임대 기간(lease)이 설정되어 있으며, 이 기간이 끝나면 해당 비밀은 자동으로 폐기됩니다. 클라이언트는 내장된 갱신 API를 통해 임대를 연장할 수 있습니다.
폐기 (Revocation):
Vault는 비밀 폐기를 기본적으로 지원합니다. 단일 비밀뿐만 아니라 특정 사용자에 의해 읽힌 모든 비밀, 또는 특정 유형의 모든 비밀 등 비밀의 계층 구조 전체를 폐기할 수 있습니다. 이 기능은 키 롤링이나 침입 발생 시 시스템을 신속하게 차단하는 데 유용합니다.

Kubernetes(Kind) 환경에 HashiCorp Vault를 Helm Chart를 통해 설치하고, 초기화(Unseal)하는 과정입니다.
[참고] Vault Kubernetes 클러스터 외부에 설치할 경우: learn-vault-external-kubernetes
# Create a Kubernetes namespace.
kubectl create namespace vault
# View all resources in a namespace.
kubectl get all --namespace vault
# Setup Helm repo
helm repo add hashicorp https://helm.releases.hashicorp.com
# Check that you have access to the chart.
helm search repo hashicorp/vault
# NAME CHART VERSION APP VERSION DESCRIPTION
# hashicorp/vault 0.30.0 1.19.0 Official HashiCorp Vault Chart
# hashicorp/vault-secrets-gateway 0.0.2 0.1.0 A Helm chart for Kubernetes
# hashicorp/vault-secrets-operator 0.10.0 0.10.0 Official Vault Secrets Operator Chart

cat <<EOF > override-values.yaml
global:
enabled: true
tlsDisable: true # Disable TLS for demo purposes
server:
image:
repository: "hashicorp/vault"
tag: "1.19.0"
standalone:
enabled: true
replicas: 1
config: |
ui = true
listener "tcp" {
address = "[::]:8200"
cluster_address = "[::]:8201"
tls_disable = 1
}
storage "file" {
path = "/vault/data"
}
service:
enabled: true
type: NodePort
port: 8200
targetPort: 8200
nodePort: 30000 # Kind에서 열어둔 포트 중 하나 사용
injector:
enabled: true
EOF
standalone(단일 인스턴스)로 Vault를 실행합니다. Production 환경에서는 standalone으로 사용하지 않는 것을 권장하며, 고가용성을 위해 홀수로 구성하는 것을 권장합니다.8200 포트를 NodePort:30000으로 노출시킵니다. # Helm Install 실행
helm upgrade vault hashicorp/vault -n vault -f override-values.yaml --install
# 네임스페이스 변경 : vault
kubens vault
Context "kind-myk8s" modified.
Active namespace is "vault".
# 배포 확인
k get pods,svc,pvc
NAME READY STATUS RESTARTS AGE
pod/vault-0 0/1 ContainerCreating 0 11s
pod/vault-agent-injector-56459c7545-9n94t 0/1 ContainerCreating 0 11s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/vault NodePort 10.96.36.121 <none> 8200:30000/TCP,8201:31091/TCP 11s
service/vault-agent-injector-svc ClusterIP 10.96.240.81 <none> 443/TCP 11s
service/vault-internal ClusterIP None <none> 8200/TCP,8201/TCP 11s
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
persistentvolumeclaim/data-vault-0 Bound pvc-6f5739c5-14e9-4a62-bed2-b98fd327bcb1 10Gi RWO standard <unset> 11s



0/1이고, describe로 Event를 살펴보면 Readiness probe failed: Key Value가 발생합니다.Vault는 기본적으로 Sealed 상태에서 시작되며, 보안 상의 이유로 반드시 초기화(init) 및 잠금 해제(unseal) 작업을 수행해야 합니다.
# Vault Status 명령으로 Sealed 상태확인
kubectl exec -ti vault-0 -- vault status

Seal Type: shamir : Shamir Secret Sharing 방식key-shares=5, key-threshold=3 등으로 설정하여 5개의 키 중 3개를 모아야 잠금 해제할 수 있도록 합니다.Sealed: true : 아직 Unseal이 되지 않았음을 의미초기화와 Unseal을 자동화하는 스크립트를 작성합니다.
cat <<EOF > init-unseal.sh
#!/bin/bash
# Vault Pod 이름
VAULT_POD="vault-0"
# Vault 명령 실행
VAULT_CMD="kubectl exec -ti \$VAULT_POD -- vault"
# 출력 저장 파일
VAULT_KEYS_FILE="./vault-keys.txt"
UNSEAL_KEY_FILE="./vault-unseal-key.txt"
ROOT_TOKEN_FILE="./vault-root-token.txt"
# Vault 초기화 (Unseal Key 1개만 생성되도록 설정)
\$VAULT_CMD operator init -key-shares=1 -key-threshold=1 | sed \$'s/\\x1b\\[[0-9;]*m//g' | tr -d '\r' > "\$VAULT_KEYS_FILE"
# Unseal Key / Root Token 추출
grep 'Unseal Key 1:' "\$VAULT_KEYS_FILE" | awk -F': ' '{print \$2}' > "\$UNSEAL_KEY_FILE"
grep 'Initial Root Token:' "\$VAULT_KEYS_FILE" | awk -F': ' '{print \$2}' > "\$ROOT_TOKEN_FILE"
# Unseal 수행
UNSEAL_KEY=\$(cat "\$UNSEAL_KEY_FILE")
\$VAULT_CMD operator unseal "\$UNSEAL_KEY"
# 결과 출력
echo "[🔓] Vault Unsealed!"
echo "[🔐] Root Token: \$(cat \$ROOT_TOKEN_FILE)"
EOF
# 실행 권한 부여
chmod +x init-unseal.sh
# 실행
./init-unseal.sh
operator init: Vault를 최초 초기화key-shares=1, key-threshold=1: 실습 편의를 위해 1개의 Unseal Key만 생성vault-keys.txt: 전체 키 및 토큰 정보 저장vault-unseal-key.txt, vault-root-token.txt: 필요한 정보만 분리 저장
kubectl exec -ti vault-0 -- vault status
# 출력 예시
Key Value
--- -----
Seal Type shamir
Initialized true
Sealed false
Total Shares 1
Threshold 1
Version 1.19.0
Build Date 2025-03-04T12:36:40Z
Storage Type file
Cluster Name vault-cluster-26e23c75
Cluster ID 326975b8-4907-2d62-9da1-f3165d7cc0c6
HA Enabled false
Sealed: false로 표시되면 성공적으로 Unseal이 완료된 상태입니다.

Vault는 UI 모드를 활성화한 상태이며, Helm 설정에서 포트 30000으로 NodePort로 노출되어 있습니다.
http://127.0.0.1:30000
또는
http://<WSL2 Ubuntu IP>:30000
vault-root-token.txt 파일에서 추출한 Root Token을 사용하여 로그인합니다.
wget -O - https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install vault
vault --version # 설치 확인
# NodePort로 공개한 30000 Port로 설정
export VAULT_ADDR='http://localhost:30000'
# vault 상태확인
vault status
# Root Token으로 로그인
vault login
Token (will be hidden):
Success! You are now authenticated. The token information displayed below
is already stored in the token helper. You do NOT need to run "vault login"
again. Future Vault requests will automatically use this token.
Key Value
--- -----
token hvs.egeVgnhMKgnFqof6YLfiPtLa
token_accessor wWpBsO3TqQEybFF0le2YmNGX
token_duration ∞
token_renewable false
token_policies ["root"]
identity_policies []
policies ["root"]


이번 실습에서는 버전 관리가 가능한 KV Version 2를 활성화하여 Static Secret 데이터를 등록하고 조회하는 과정을 진행합니다.
Vault CLI를 사용하여 KV Version 2 형태의 시크릿 엔진을 활성화합니다. 활성화 경로는 /secret 으로 지정합니다.
/secret/sampleapp/config 경로에 Key-Value 형태의 시크릿 데이터를 저장합니다.
# KV v2 형태로 엔진 활성화
vault secrets enable -path=secret kv-v2
# 샘플 시크릿 저장
vault kv put secret/sampleapp/config \
username="demo" \
password="p@ssw0rd"
# 입력된 데이터 확인
vault kv get secret/sampleapp/config
======== Secret Path ========
secret/data/sampleapp/config
======= Metadata =======
Key Value
--- -----
created_time 2025-03-30T05:18:47.797852422Z
custom_metadata <nil>
deletion_time n/a
destroyed false
version 1
====== Data ======
Key Value
--- -----
password p@ssw0rd
username demo

Vault UI에서도 동일한 데이터를 확인할 수 있습니다.
[Secrets Engine] 탭에 접속 후 [sampleapp - config] 접속하여 실제 저장된 Key / Value를 확인합니다.


Vault Agent를 활용하면 Kubernetes Pod 내부에 Sidecar 형태로 Vault Agent를 배치하여, Vault로부터 시크릿 데이터를 자동으로 주입할 수 있습니다.
AppRole은 Vault에서 제공하는 인증 방법 중 하나로, Role ID와 Secret ID를 이용해 Vault에 접근 권한을 부여하는 방식입니다.
# 1. AppRole 인증 방식 활성화
vault auth enable approle || echo "AppRole already enabled"
vault auth list
# 2. 정책 생성
# secret/data/sampleapp/ 하위 경로의 시크릿 데이터에 대해 읽기 권한만 허용
vault policy write sampleapp-policy - <<EOF
path "secret/data/sampleapp/*" {
capabilities = ["read"]
}
EOF
# 3. AppRole Role 생성
vault write auth/approle/role/sampleapp-role \
token_policies="sampleapp-policy" \
secret_id_ttl="1h" \
token_ttl="1h" \
token_max_ttl="4h"
# 4. Role ID 및 Secret ID 추출 및 저장
ROLE_ID=$(vault read -field=role_id auth/approle/role/sampleapp-role/role-id)
SECRET_ID=$(vault write -f -field=secret_id auth/approle/role/sampleapp-role/secret-id)
echo "ROLE_ID: $ROLE_ID"
echo "SECRET_ID: $SECRET_ID"
# 5. 파일로 저장
mkdir -p approle-creds
echo "$ROLE_ID" > approle-creds/role_id.txt
echo "$SECRET_ID" > approle-creds/secret_id.txt
# 6. (옵션) Kubernetes Secret으로 저장
kubectl create secret generic vault-approle -n vault \
--from-literal=role_id="${ROLE_ID}" \
--from-literal=secret_id="${SECRET_ID}" \
--save-config \
--dry-run=client -o yaml | kubectl apply -f -

Vault Agent는 설정 파일(agent-config.hcl)을 기반으로 Vault에 접근하고, 시크릿 데이터를 템플릿화 하여 특정 파일로 렌더링합니다.
cat <<EOF | kubectl create configmap vault-agent-config -n vault --from-file=agent-config.hcl=/dev/stdin --dry-run=client -o yaml | kubectl apply -f -
vault {
address = "http://vault.vault.svc:8200"
}
auto_auth {
method "approle" {
config = {
role_id_file_path = "/etc/vault/approle/role_id"
secret_id_file_path = "/etc/vault/approle/secret_id"
remove_secret_id_file_after_reading = false
}
}
sink "file" {
config = {
path = "/etc/vault-agent-token/token"
}
}
}
template_config {
static_secret_render_interval = "20s"
}
template {
destination = "/etc/secrets/index.html"
contents = <<EOH
<html>
<body>
<p>username: {{ with secret "secret/data/sampleapp/config" }}{{ .Data.data.username }}{{ end }}</p>
<p>password: {{ with secret "secret/data/sampleapp/config" }}{{ .Data.data.password }}{{ end }}</p>
</body>
</html>
EOH
}
EOF

kubectl apply -n vault -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-vault-demo
spec:
replicas: 1
selector:
matchLabels:
app: nginx-vault-demo
template:
metadata:
labels:
app: nginx-vault-demo
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
volumeMounts:
- name: html-volume
mountPath: /usr/share/nginx/html
- name: vault-agent-sidecar
image: hashicorp/vault:latest
args:
- "agent"
- "-config=/etc/vault/agent-config.hcl"
volumeMounts:
- name: vault-agent-config
mountPath: /etc/vault
- name: vault-approle
mountPath: /etc/vault/approle
- name: vault-token
mountPath: /etc/vault-agent-token
- name: html-volume
mountPath: /etc/secrets
volumes:
- name: vault-agent-config
configMap:
name: vault-agent-config
- name: vault-approle
secret:
secretName: vault-approle
- name: vault-token
emptyDir: {}
- name: html-volume
emptyDir: {}
EOF

kubectl apply -f - <<EOF
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
type: NodePort
selector:
app: nginx-vault-demo
ports:
- protocol: TCP
port: 80
targetPort: 80
nodePort: 30001 # Kind에서 설정한 Port
EOF

# 파드 내에 사이드카 컨테이너 추가되어 2/2 확인
kubectl get pod -l app=nginx-vault-demo
NAME READY STATUS RESTARTS AGE
nginx-vault-demo-7776649597-tcnxd 2/2 Running 0 5m32s
kubectl describe pod -l app=nginx-vault-demo
...
Containers:
nginx:
Container ID: containerd://c160c2268ce6d7602b718336b9036ae89646b5fdb7ebf310bed3dcf497b8675e
Image: nginx:latest
...
vault-agent-sidecar:
Container ID: containerd://6d7e37e8925e681d163b06ae7d18f5d3a236164a60ebf422abd4229a2e337e4e
Image: hashicorp/vault:latest
Image ID: docker.io/hashicorp/vault@sha256:ee674e47dcf85849aadf255b5341f76c0e1a474bc5fa9be9cdfff2a2edf9a628
Port: <none>
Host Port: <none>
Args:
agent
-config=/etc/vault/agent-config.hcl
...
# mutating admission
kubectl get mutatingwebhookconfigurations.admissionregistration.k8s.io
NAME WEBHOOKS AGE
vault-agent-injector-cfg 1 3h10m






Jenkins가 Vault와 연동하기 위해서는 AppRole 방식 인증을 사용합니다. Vault에서 발급된 ROLE_ID와 SECRET_ID는 기존에 생성한 파일(role_id.txt, secret_id.txt)을 참고하거나 아래 명령어로 다시 조회할 수 있습니다.
# Role ID
vault read auth/approle/role/<role-name>/role-id
# Secret ID
vault write -f auth/approle/role/<role-name>/secret-id
# 예시
vault read auth/approle/role/sampleapp-role/role-id
Key Value
--- -----
role_id 678c0c6e-57df-bb23-1427-f6318843a514
Vault Plugin을 활용하여 Jenkins가 Vault와 통신하도록 설정합니다.


vault-approle-creds 등) 

New Item 클릭jenkins-vault-kv)
Vault Plugin을 활용한 Jenkinsfile 예시는 다음과 같습니다.
pipeline {
agent any
environment {
VAULT_ADDR = 'http://192.168.0.2:30000' // 실제 Vault 주소로 변경!!!
}
stages {
stage('Read Vault Secret') {
steps {
withVault([
vaultSecrets: [
[
path: 'secret/sampleapp/config',
engineVersion: 2,
secretValues: [
[envVar: 'USERNAME', vaultKey: 'username'],
[envVar: 'PASSWORD', vaultKey: 'password']
]
]
],
configuration: [
vaultUrl: "${VAULT_ADDR}",
vaultCredentialId: 'vault-approle-creds'
]
]) {
sh '''
echo "Username from Vault: $USERNAME"
echo "Password from Vault: $PASSWORD"
'''
script {
echo "Username (env): ${env.USERNAME}"
echo "Password (env): ${env.PASSWORD}"
}
}
}
}
}
}


Vault Plugin 사용 시 Jenkins Console Output에 출력되는 시크릿 값은 보안상 자동으로 마스킹 처리됩니다.
출력되는 Username/Password 값은 별도로 파일로 저장하거나, 추가 로깅 설정 시 우회하여 확인할 수 있습니다.

ArgoCD Vault Plugin이 Vault 서버에 접근하기 위해 필요한 ROLE_ID, SECRET_ID 정보를 활용하여 Kubernetes Secret을 생성합니다. ROLE_ID와 SECRET_ID는 이전 Vault 실습 과정에서 생성한 값을 그대로 사용합니다.
kubectl apply -f - <<EOF
kind: Secret
apiVersion: v1
metadata:
name: argocd-vault-plugin-credentials
namespace: argocd
type: Opaque
stringData:
VAULT_ADDR: "http://vault.vault:8200"
AVP_TYPE: "vault"
AVP_AUTH_TYPE: "approle"
AVP_ROLE_ID: 88e405be-2485-2f4c-44de-0f1c929573d3 #Role_ID
AVP_SECRET_ID: 4dea7bbb-85f4-b81d-43a4-81dfc2b8ed9d #Secret_ID
EOF

이번 구성은 Helm으로 배포된 ArgoCD 환경에 Kustomize를 활용하여 Patch 적용 방식으로 Sidecar Plugin을 구성합니다. Sidecar 방식은 ArgoCD의 argocd-repo-server Deployment에 Plugin Container를 추가하는 방식입니다. 이번 실습은 AppRole 방식을 사용하므로 별도의 ServiceAccount Token 구성은 진행하지 않습니다.
git clone https://github.com/hyungwook0221/argocd-vault-plugin.git
cd argocd-vault-plugin/manifests/cmp-sidecar
# argocd 네임스페이스 설정
kubens argocd
# 생성될 메니페스트 파일에 대한 확인
kubectl kustomize .
# -k 옵션으로 kusomize 실행
kubectl apply -n argocd -k .



k exec -it -n vault vault-0 -- sh
# vault pod shell에 접속 후 root token으로 로그인
vault login
Token (will be hidden): <토큰입력>
# 확인명령
vault read auth/kubernetes/role/argocd
exit

Vault에 저장된 시크릿 데이터를 ArgoCD 배포 단계에서 Helm Values 파일 내 변수로 치환하여 배포합니다.
kubectl apply -n argocd -f - <<EOF
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: demo
namespace: argocd
spec:
destination:
namespace: argocd
server: https://kubernetes.default.svc
project: default
source:
path: infra/helm
repoURL: https://github.com/hyungwook0221/spring-boot-debug-app
targetRevision: main
plugin:
name: argocd-vault-plugin-helm
env:
- name: HELM_ARGS
value: -f new-values.yaml
syncPolicy:
automated:
prune: true
selfHeal: true
EOF

serviceAccount:
create: true
image:
repository: luafanti/spring-boot-debug-app
tag: main
pullPolicy: IfNotPresent
replicaCount: 1
resources:
memoryRequest: 256Mi
memoryLimit: 512Mi
cpuRequest: 500m
cpuLimit: 1
probes:
liveness:
initialDelaySeconds: 15
path: /actuator/health/liveness
failureThreshold: 3
successThreshold: 1
timeoutSeconds: 3
periodSeconds: 5
readiness:
initialDelaySeconds: 15
path: /actuator/health/readiness
failureThreshold: 3
successThreshold: 1
timeoutSeconds: 3
periodSeconds: 5
ports:
http:
name: http
value: 8080
management:
name: management
value: 8081
envs:
- name: VAULT_SECRET_USER
value: <path:secret/data/sampleapp/config#username>
- name: VAULT_SECRET_PASSWORD
value: <path:secret/data/sampleapp/config#password>
log:
level:
spring: "info"
service: "info"



Deployment에 적용된 env 값 확인

envs:
- name: VAULT_SECRET_USER
value: <path:secret/data/sampleapp/config#username>
- name: VAULT_SECRET_PASSWORD
value: <path:secret/data/sampleapp/config#password>
# vault-operator-values.yaml
defaultVaultConnection:
enabled: true
address: "http://vault.vault.svc.cluster.local:8200"
skipTLSVerify: false
controller:
manager:
clientCache:
persistenceModel: direct-encrypted
storageEncryption:
enabled: true
mount: k8s-auth-mount
keyName: vso-client-cache
transitMount: demo-transit
kubernetes:
role: auth-role-operator
serviceAccount: vault-secrets-operator-controller-manager
tokenAudiences: ["vault"]
helm install vault-secrets-operator hashicorp/vault-secrets-operator \
-n vault-secrets-operator-system \
--create-namespace \
--values vault-operator-values.yaml

kubectl create ns postgres
helm repo add bitnami https://charts.bitnami.com/bitnami
helm upgrade --install postgres bitnami/postgresql \
--namespace postgres \
--set auth.audit.logConnections=true \
--set auth.postgresPassword=secret-pass

kubectl exec --stdin=true --tty=true vault-0 -n vault -- /bin/sh
# 최초 설치시 획득한 vault root token 사용.
# 예시) hvs.hnlFKWjE10FOyrwLMeK9PrCC
vault login
# Kubernetes 인증 메서드 활성화
vault auth enable -path k8s-auth-mount kubernetes
# Kubernetes 클러스터 정보 구성
vault write auth/k8s-auth-mount/config \
kubernetes_host="https://kubernetes.default.svc:443" \
kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
token_reviewer_jwt="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)"
# 설정 확인
vault read auth/k8s-auth-mount/config

vault write auth/k8s-auth-mount/role/auth-role \
bound_service_account_names=demo-dynamic-app \
bound_service_account_namespaces=demo-ns \
token_ttl=0 \
token_period=120 \
token_policies=demo-auth-policy-db \
audience=vault

# demo-db라는 경로로 Database Secret Engine을 활성화
vault secrets enable -path=demo-db database
# PostgreSQL 연결 정보 등록
# 해당 과정은 postgres가 정상적으로 동작 시 적용 가능
# allowed_roles: 이후 설정할 Role 이름 지정
vault write demo-db/config/demo-db \
plugin_name=postgresql-database-plugin \
allowed_roles="dev-postgres" \
connection_url="postgresql://{{username}}:{{password}}@postgres-postgresql.postgres.svc.cluster.local:5432/postgres?sslmode=disable" \
username="postgres" \
password="secret-pass"
# DB 사용자 동적 생성 Role 등록
# 해당 Role 사용 시 Vault가 동적으로 사용자 계정과 비밀번호를 생성 가능
# TTL은 생성된 자격증명의 유효 시간 (30초~10분)
vault write demo-db/roles/dev-postgres \
db_name=demo-db \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \
GRANT ALL PRIVILEGES ON DATABASE postgres TO \"{{name}}\";" \
revocation_statements="REVOKE ALL ON DATABASE postgres FROM \"{{name}}\";" \
backend=demo-db \
name=dev-postgres \
default_ttl="1m" \
max_ttl="1m"
# 정책 설정: DB 자격증명 읽기 권한
# demo-db/creds/dev-postgres 경로에 대한 read 권한 부여
# 추후 Kubernetes 서비스 어카운트(demo-dynamic-app)에 이 정책을 연결해서 자격증명 요청 가능
vault policy write demo-auth-policy-db - <<EOF
path "demo-db/creds/dev-postgres" {
capabilities = ["read"]
}
EOF

kubectl exec --stdin=true --tty=true vault-0 -n vault -- /bin/sh
# Transit Secret Engine 활성화
# transit 엔진을 demo-transit 경로로 활성화.
# - 데이터를 저장하지 않고 암복호화 기능만 제공하는 Vault의 기능
vault secrets enable -path=demo-transit transit
# vso-client-cache라는 키를 생성
# 이 키는 VSO가 암복호화 시 사용할 암호화 키 역할
vault write -force demo-transit/keys/vso-client-cache
# vso-client-cache 키에 대해 암호화(encrypt), 복호화(decrypt)를 허용하는 정책 생성
vault policy write demo-auth-policy-operator - <<EOF
path "demo-transit/encrypt/vso-client-cache" {
capabilities = ["create", "update"]
}
path "demo-transit/decrypt/vso-client-cache" {
capabilities = ["create", "update"]
}
EOF
# Vault Secrets Operator가 사용하는 ServiceAccount에 위 정책을 바인딩
# vso가 Vault에 로그인할 때 사용할 수 있는 JWT 기반 Role 설정
# 해당 Role을 통해 Operator는 Transit 엔진을 이용한 암복호화 API 호출 가능
vault write auth/k8s-auth-mount/role/auth-role-operator \
bound_service_account_names=vault-secrets-operator-controller-manager \
bound_service_account_namespaces=vault-secrets-operator-system \
token_ttl=0 \
token_period=120 \
token_policies=demo-auth-policy-operator \
audience=vault
vault read auth/k8s-auth-mount/role/auth-role-operator


kubectl create ns demo-ns
mkdir vso-dynamic
cd vso-dynamic
---
# vault-auth-dynamic.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
namespace: demo-ns
name: demo-dynamic-app
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultAuth
metadata:
name: dynamic-auth
namespace: demo-ns
spec:
method: kubernetes
mount: k8s-auth-mount
kubernetes:
role: auth-role
serviceAccount: demo-dynamic-app
audiences:
- vault
---
# app-secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: vso-db-demo
namespace: demo-ns
---
# vault-dynamic-secret.yaml
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultDynamicSecret
metadata:
name: vso-db-demo
namespace: demo-ns
spec:
refreshAfter: 25s
mount: demo-db
path: creds/dev-postgres
destination:
name: vso-db-demo
create: true
overwrite: true
vaultAuthRef: dynamic-auth
rolloutRestartTargets:
- kind: Deployment
name: vaultdemo
---
# app-spring-deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: vaultdemo
namespace: demo-ns
labels:
app: vaultdemo
spec:
replicas: 1
selector:
matchLabels:
app: vaultdemo
template:
metadata:
labels:
app: vaultdemo
spec:
volumes:
- name: secrets
secret:
secretName: "vso-db-demo"
containers:
- name: vaultdemo
image: hyungwookhub/vso-spring-demo:v5
imagePullPolicy: IfNotPresent
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: "vso-db-demo"
key: password
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: "vso-db-demo"
key: username
- name: DB_HOST
value: "postgres-postgresql.postgres.svc.cluster.local"
- name: DB_PORT
value: "5432"
- name: DB_NAME
value: "postgres"
ports:
- containerPort: 8088
volumeMounts:
- name: secrets
mountPath: /etc/secrets
readOnly: true
---
apiVersion: v1
kind: Service
metadata:
name: vaultdemo
namespace: demo-ns
spec:
ports:
- name: vaultdemo
port: 8088
targetPort: 8088
nodePort: 30003
selector:
app: vaultdemo
type: NodePort
kubectl apply -f .

localhost:30003 접속 후 K8s Secrets에 생성된 username, password 값으로 연결을 테스트합니다.
