집에서 MSA 환경을 k8s로 만들기
이전회사에서 일할 때 MSA 환경이 정말 잘 갖춰져 있었다. 이후 퇴사하고나서 Monolithic 사용, 운영 하다보니 마음 한편에 "MSA 환경을 처음부터 끝까지 한번 만들어보고 싶다"는 생각이 계속 들었고, 뭘 만들까 고민하다가 블로그를 골랐다. User 서비스, Post 서비스 정도면 MSA 구조 잡기에 충분하니까.
왜 집에서?
처음엔 Vultr나 DigitalOcean 같은 클라우드를 생각했다. 근데 계산해보니까 MSA로 서비스 3-4개 띄우고, DB 붙이고, 모니터링까지 하면 월 $50은 훌쩍 넘더라. 그냥 사이드 프로젝트인데 그건 좀 아까웠다.
그래서 집에 굴러다니던 Synology DS718+ 를 꺼냈다. 원래 안쓰고 있엇는데, 전원버튼만 누르면 개인 서버가 완성됐다.
Synology에서 Docker를 직접 돌릴 수도 있지만, VMM(Virtual Machine Manager) 으로 Ubuntu VM을 올렸다. 이유:
- Docker Desktop보다 네이티브 Docker가 안정적
- SSH로 접속해서 일반 서버처럼 관리 가능
- 스냅샷 기능으로 롤백도 쉬움
Ubuntu 22.04 LTS를 설치하고, Docker랑 kubectl만 깔면 준비 끝이다.
Kind로 K8s 클러스터 구축
IaC 환경을 구성해보고 싶었고, 지금은 많이 익숙한 Kubernetes를 선택했다. 물론 NAS 하나에 프로덕션급 클러스터를 올릴 순 없으니까 Kind(Kubernetes in Docker) 로 구성했다.
Kind는 Docker 컨테이너 안에서 K8s 노드를 실행하는 방식이다. 가볍고 빠르게 클러스터를 만들 수 있어서 로컬 개발이나 CI 환경에서 많이 쓴다. 단점이라면 단일노드라는 점이지만, 어차피 나에게는 별 문제가 되진 않았다.
클러스터 설정
# kind-config.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
networking:
apiServerAddress: "0.0.0.0" # 외부에서 접근 가능하도록
apiServerPort: 6443
nodes:
- role: control-plane
kubeadmConfigPatches:
- |
kind: InitConfiguration
nodeRegistration:
kubeletExtraArgs:
node-labels: "ingress-ready=true"
extraPortMappings:
- containerPort: 80
hostPort: 80
protocol: TCP
- containerPort: 443
hostPort: 443
protocol: TCP
- containerPort: 5432 # PostgreSQL
hostPort: 5432
- containerPort: 6379 # Redis
hostPort: 6379
핵심 포인트:
apiServerAddress: "0.0.0.0": NAS 외부(내 맥북)에서 kubectl로 접근하려면 필요하다extraPortMappings: Kind 컨테이너 안의 포트를 호스트로 노출한다. DB 접근용으로 5432, 6379도 열어뒀다ingress-ready=true: nginx ingress를 설치하기 위한 라벨
클러스터 생성 스크립트
매번 명령어 치기 귀찮아서 스크립트로 만들어뒀다:
#!/bin/bash
set -e
# 기존 클러스터 삭제
if kind get clusters 2>/dev/null | grep -q "flatcoke"; then
echo "기존 클러스터 삭제 중..."
kind delete cluster --name flatcoke
fi
# 클러스터 생성
kind create cluster --name flatcoke --config=kind-config.yaml
# nginx ingress 설치
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml
# ingress 준비 대기
kubectl wait --namespace ingress-nginx \
--for=condition=ready pod \
--selector=app.kubernetes.io/name=ingress-nginx \
--timeout=90s
echo "완료! 열린 포트: 80, 443, 5432, 6379"
스크립트 하나로 클러스터 생성부터 ingress 설치까지 끝난다. 뭔가 꼬이면 그냥 다시 돌리면 된다.
GitHub Container Registry
Docker Hub나 ECR 대신 ghcr.io를 썼다. 이유:
- GitHub Actions랑 연동이 자연스러움
- private repo도 무료
- 이미 GitHub에 코드가 있으니까 한 곳에서 관리
각 서비스의 Dockerfile을 빌드해서 ghcr.io에 푸시하면, K8s deployment에서 바로 당겨올 수 있다:
# deployment.yaml
spec:
containers:
- name: post
image: ghcr.io/flatcoke/post:latest
imagePullSecrets:
- name: github-regcred # GitHub PAT으로 만든 시크릿
private 이미지를 당기려면 imagePullSecrets가 필요하다. GitHub PAT(Personal Access Token)으로 시크릿을 만들어두면 된다:
kubectl create secret docker-registry github-regcred \
--namespace=post \
--docker-server=ghcr.io \
--docker-username=<GITHUB_USERNAME> \
--docker-password=<GITHUB_PAT> \
--docker-email=<EMAIL>
SOPS로 시크릿 관리
AWS SSM이나 Secrets Manager가 없으니까 대안이 필요했다. SOPS를 선택한 이유:
- Git에 암호화된 시크릿을 커밋할 수 있음
- 버전 관리가 됨
- 별도의 시크릿 서버가 필요 없음
설정 방법
먼저 age 키를 생성한다:
brew install sops age
age-keygen -o ~/.config/sops/age/keys.txt
생성된 키 파일은 이렇게 생겼다:
# created: 2024-01-01T00:00:00+09:00
# public key: age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
AGE-SECRET-KEY-1XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
public key는 암호화할 때 쓰고, secret key는 복호화할 때 쓴다. secret key는 절대 Git에 커밋하면 안 된다.
시크릿 암호화
# 평문 시크릿 작성 (임시 파일)
cat > /tmp/db-secret.yaml <<EOF
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
namespace: post
stringData:
POSTGRES_PASSWORD: "my-secret-password"
POSTGRES_USER: "postgres"
EOF
# 암호화 (.sops.yaml에 age public key 설정 필요)
sops -e /tmp/db-secret.yaml > k8s/secrets/db-secret.yaml
# 원본 삭제
rm /tmp/db-secret.yaml
암호화된 파일은 이렇게 생겼다:
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
namespace: post
stringData:
POSTGRES_PASSWORD: ENC[AES256_GCM,data:xxxxxxxxxxxxxxxx,iv:xxx,tag:xxx,type:str]
POSTGRES_USER: ENC[AES256_GCM,data:xxxxxxxx,iv:xxx,tag:xxx,type:str]
sops:
age:
- recipient: age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBxxxxxxxxx
-----END AGE ENCRYPTED FILE-----
lastmodified: "2024-01-01T00:00:00Z"
mac: ENC[AES256_GCM,data:xxx,iv:xxx,tag:xxx,type:str]
version: 3.8.1
stringData 값들이 ENC[...]로 암호화되어 있다. 이 파일은 Git에 커밋해도 안전하다.
시크릿 적용
적용할 때는 복호화해서 kubectl에 파이프:
sops -d k8s/secrets/db-secret.yaml | kubectl apply -f -
편하게 쓰려고 스크립트로 만들어뒀다:
#!/bin/bash
# apply.sh - 모든 암호화된 시크릿 적용
for file in k8s/secrets/*.yaml; do
echo "Applying $file..."
sops -d "$file" | kubectl apply -f -
done
시크릿 수정
암호화된 파일을 직접 수정하려면:
sops k8s/secrets/db-secret.yaml
에디터가 열리면서 복호화된 내용이 보인다. 수정하고 저장하면 자동으로 다시 암호화된다.
Helm Chart로 템플릿화
MSA는 반복의 연속이다. User 서비스, Post 서비스, 나중에 추가될 서비스들... 전부 비슷한 구조다:
- Deployment
- Service
- Ingress
- HPA (Horizontal Pod Autoscaler)
- PDB (Pod Disruption Budget)
서비스마다 이걸 다 복붙하면 유지보수가 지옥이 된다. 그래서 공통 Helm Chart 템플릿을 만들었다.
구조
iac/k8s/
+-- charts/
| +-- microservice/ # 공통 템플릿
| +-- Chart.yaml
| +-- values.yaml # 기본값
| +-- templates/
| +-- deployment.yaml
| +-- service.yaml
| +-- ingress.yaml
| +-- hpa.yaml
| +-- pdb.yaml
| +-- namespace.yaml
+-- user/
| +-- helm/
| +-- values.yaml # User 서비스 설정만
+-- post/
+-- helm/
+-- values.yaml # Post 서비스 설정만
공통 템플릿 (values.yaml)
# charts/microservice/values.yaml (일부)
name: ""
image:
repository: ghcr.io/flatcokecom
tag: latest
pullPolicy: Always
imagePullSecrets:
- name: github-regcred
ports:
http: 8080
grpc: 9090
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
probes:
readiness:
path: /health
port: 8080
liveness:
path: /health
port: 8080
ingress:
enabled: true
className: nginx
hpa:
enabled: true
minReplicas: 1
maxReplicas: 3
targetCPUUtilizationPercentage: 70
서비스별 설정
각 서비스는 자기만의 설정만 정의하면 된다:
# user/helm/values.yaml
name: user
namespace:
create: true
env:
ENV: "prod"
secret:
name: user-secret
dbKeys:
- DB_HOST
- DB_PORT
- DB_USER
- DB_PASSWORD
- DB_NAME
ingress:
host: user.flatcoke.com
# post/helm/values.yaml
name: post
namespace:
create: true
env:
ENV: "prod"
GRPC_ADDR: "0.0.0.0:9090"
HTTP_ADDR: "0.0.0.0:8080"
secret:
name: post-secret
dbKeys:
- DB_HOST
- DB_PORT
- DB_USER
- DB_PASSWORD
- DB_NAME
extraKeys:
- R2_ACCESS_KEY_ID
- R2_SECRET_ACCESS_KEY
extraEnv:
USER_SERVICE_ADDR: "user-service.user.svc.cluster.local:9090"
ingress:
host: post.flatcoke.com
배포
# User 서비스 배포
helm upgrade --install user ./charts/microservice \
-f ./user/helm/values.yaml \
--namespace user
# Post 서비스 배포
helm upgrade --install post ./charts/microservice \
-f ./post/helm/values.yaml \
--namespace post
새 서비스를 추가할 때 values.yaml 하나만 만들면 된다. Deployment, Service, Ingress, HPA, PDB 전부 자동으로 생성된다.
마무리
집에 있는 NAS에 Kind로 K8s를 올리고, SOPS로 시크릿을 관리하고, ghcr.io로 이미지를 배포하는 환경을 구축했다.
클라우드 없이도 MSA 인프라를 구축할 수 있다는 걸 알게 됐다. 물론 프로덕션에서는 이렇게 안 하겠지만, 사이드 프로젝트로 경험해보기엔 충분하다.