집에서 MSA 환경을 k8s로 만들기
이전회사에서 일할 때 MSA 환경이 정말 잘 갖춰져 있었다. 이후 퇴사하고나서 Monolithic 사용, 운영 하다보니 마음 한편에 "MSA 환경을 처음부터 끝까지 한번 만들어보고 싶다"는 생각이 계속 들었고, 뭘 만들까 고민하다가 블로그를 골랐다. User 서비스, Post 서비스 정도면 MSA 구조 잡기에 충분하니까.
왜 집에서?
처음엔 Vultr나 DigitalOcean 같은 클라우드를 생각했다. 근데 계산해보니까 MSA로 서비스 3-4개 띄우고, DB 붙이고, 모니터링까지 하면 월 $50은 훌쩍 넘더라. 그냥 사이드 프로젝트인데 그건 좀 아까웠다.
그래서 집에 있는 Mac Studio (M1 Max, 64GB RAM) 를 서버로 쓰기로 했다. K8s 런타임은 OrbStack을 선택했다. OrbStack은 macOS에서 Docker와 K8s를 네이티브로 지원하는 가벼운 런타임이다. Docker Desktop보다 훨씬 가볍고, 경량 Linux VM 위에서 직접 K8s를 돌린다.
OrbStack으로 K8s 클러스터 구축
OrbStack의 K8s는 별도 설정 없이 바로 쓸 수 있다. config 파일 작성하고, 클러스터 생성 스크립트 돌리고 할 필요가 없다. OrbStack 설정에서 Kubernetes를 켜기만 하면 된다.
외부 접근 설정
다만 하나 설정해줘야 할 게 있다. 서비스를 외부에서 접근하려면 OrbStack 설정에서 다음을 켜야 한다:
# ~/.orbstack/config/orbstack.yaml
k8s:
expose_services: true
이 설정을 켜면 K8s의 NodePort, LoadBalancer 타입 서비스가 호스트 머신에 바인딩된다. Ingress Controller를 설치하면 80/443 포트로 바로 접근할 수 있다.
Ingress 설치
# nginx ingress 설치
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/cloud/deploy.yaml
# ingress 준비 대기
kubectl wait --namespace ingress-nginx \
--for=condition=ready pod \
--selector=app.kubernetes.io/name=ingress-nginx \
--timeout=90s
OrbStack이 LoadBalancer를 네이티브로 지원하기 때문에 provider/cloud/deploy.yaml을 쓴다.
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 전부 자동으로 생성된다.
마무리
Mac Studio에서 OrbStack K8s를 돌리고, SOPS로 시크릿을 관리하고, ghcr.io로 이미지를 배포하는 환경을 구축했다.
클라우드 없이도 MSA 인프라를 구축할 수 있다는 걸 알게 됐다. 물론 프로덕션에서는 이렇게 안 하겠지만, 사이드 프로젝트로 경험해보기엔 충분하다.