⏱️ 예상 읽기 시간: 25분
서론
첫 번째 글에서 Plane의 기본 설치를, 두 번째 글에서 GitHub 연동과 API 자동화를 다뤘다면, 이번에는 엔터프라이즈급 운영을 위한 쿠버네티스 배포에 집중하겠습니다.
이 가이드에서는 개발부터 운영까지 전체 라이프사이클을 다룹니다:
- 🖥️ OrbStack 개발 환경: macOS에서 빠른 쿠버네티스 테스트
- ⚙️ Helm 차트 구성: 재사용 가능한 배포 패키지
- ☁️ 클라우드 운영: AWS EKS, GCP GKE 실전 배포
- 📊 모니터링 & 로깅: Prometheus, Grafana, ELK 스택
- 🔒 보안 & 백업: RBAC, 네트워크 정책, 데이터 보호
- 🚀 CI/CD 파이프라인: GitOps로 자동 배포
OrbStack 개발 환경 설정
1. OrbStack이란?
OrbStack은 macOS에서 Docker와 쿠버네티스를 실행하는 혁신적인 도구입니다. Docker Desktop의 강력한 대안으로, 10배 빠른 시작 속도와 2배 적은 메모리 사용량을 자랑합니다.
주요 특징
- 네이티브 성능: Apple Silicon 최적화
- 빠른 시작: 5초 이내 컨테이너 실행
- 쿠버네티스 내장: 별도 설치 없이 K8s 클러스터 제공
- 통합 네트워킹: localhost로 직접 접근 가능
2. OrbStack 설치
# Homebrew를 통한 설치
brew install orbstack
# 또는 공식 웹사이트에서 다운로드
# https://orbstack.dev/download
# 설치 확인
orb version
3. 쿠버네티스 클러스터 활성화
# OrbStack 실행 후 쿠버네티스 활성화
orb create --kubernetes my-plane-cluster
# kubectl 컨텍스트 확인
kubectl config current-context
# 클러스터 상태 확인
kubectl cluster-info
kubectl get nodes
OrbStack의 장점:
# 즉시 사용 가능한 LoadBalancer
kubectl get svc -A | grep LoadBalancer
# 빠른 이미지 빌드 (BuildKit 지원)
docker build -t plane-test . --platform linux/arm64
Plane Kubernetes 매니페스트 작성
1. 네임스페이스 및 기본 리소스
# namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: plane
labels:
name: plane
app: plane-project-management
---
# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: plane-config
namespace: plane
data:
# 환경 설정
NODE_ENV: "production"
DEBUG: "0"
CORS_ALLOWED_ORIGINS: "https://plane.yourdomain.com"
# 데이터베이스 설정
POSTGRES_HOST: "plane-postgresql"
POSTGRES_PORT: "5432"
POSTGRES_DB: "plane"
# Redis 설정
REDIS_HOST: "plane-redis"
REDIS_PORT: "6379"
# MinIO 설정
MINIO_ROOT_USER: "plane"
MINIO_ROOT_PASSWORD: "plane123"
USE_MINIO: "1"
# 애플리케이션 URL
WEB_URL: "https://plane.yourdomain.com"
ADMIN_BASE_URL: "https://plane.yourdomain.com/admin"
SPACE_BASE_URL: "https://plane.yourdomain.com/spaces"
2. Secret 관리
# secrets.yaml
apiVersion: v1
kind: Secret
metadata:
name: plane-secrets
namespace: plane
type: Opaque
data:
# Base64로 인코딩된 값들
POSTGRES_PASSWORD: cGxhbmVfcGFzc3dvcmQ= # plane_password
SECRET_KEY: c3VwZXJfc2VjcmV0X2tleV9mb3JfcGxhbmU= # super_secret_key_for_plane
GITHUB_CLIENT_SECRET: Z2l0aHViX2NsaWVudF9zZWNyZXQ= # github_client_secret
SLACK_WEBHOOK_URL: aHR0cHM6Ly9ob29rcy5zbGFjay5jb20v # https://hooks.slack.com/
---
# secret을 생성하는 스크립트
# create-secrets.sh
#!/bin/bash
kubectl create secret generic plane-secrets \
--from-literal=POSTGRES_PASSWORD="$(openssl rand -hex 32)" \
--from-literal=SECRET_KEY="$(python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())')" \
--from-literal=GITHUB_CLIENT_SECRET="your_github_client_secret" \
--from-literal=SLACK_WEBHOOK_URL="your_slack_webhook_url" \
--namespace=plane
3. Persistent Volume 설정
# storage.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: plane-postgresql-pvc
namespace: plane
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
storageClassName: fast-ssd
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: plane-minio-pvc
namespace: plane
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 50Gi
storageClassName: fast-ssd
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: plane-redis-pvc
namespace: plane
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
storageClassName: fast-ssd
PostgreSQL 배포
1. PostgreSQL StatefulSet
# postgresql.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: plane-postgresql
namespace: plane
spec:
serviceName: plane-postgresql
replicas: 1
selector:
matchLabels:
app: plane-postgresql
template:
metadata:
labels:
app: plane-postgresql
spec:
containers:
- name: postgresql
image: postgres:15.7-alpine
env:
- name: POSTGRES_DB
valueFrom:
configMapKeyRef:
name: plane-config
key: POSTGRES_DB
- name: POSTGRES_USER
value: "plane"
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: plane-secrets
key: POSTGRES_PASSWORD
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
ports:
- containerPort: 5432
name: postgresql
volumeMounts:
- name: postgresql-data
mountPath: /var/lib/postgresql/data
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "500m"
livenessProbe:
exec:
command:
- /bin/sh
- -c
- exec pg_isready -U plane -h 127.0.0.1 -p 5432
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
readinessProbe:
exec:
command:
- /bin/sh
- -c
- exec pg_isready -U plane -h 127.0.0.1 -p 5432
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
volumes:
- name: postgresql-data
persistentVolumeClaim:
claimName: plane-postgresql-pvc
---
apiVersion: v1
kind: Service
metadata:
name: plane-postgresql
namespace: plane
spec:
selector:
app: plane-postgresql
ports:
- port: 5432
targetPort: 5432
name: postgresql
type: ClusterIP
2. PostgreSQL 초기화 작업
# postgresql-init-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: plane-db-migration
namespace: plane
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: db-migration
image: makeplane/plane-backend:latest
command: ["/bin/sh"]
args:
- -c
- |
python manage.py wait_for_db
python manage.py migrate --settings=plane.settings.production
python manage.py collectstatic --noinput --settings=plane.settings.production
env:
- name: DATABASE_URL
value: "postgres://plane:$(POSTGRES_PASSWORD)@plane-postgresql:5432/plane"
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: plane-secrets
key: POSTGRES_PASSWORD
envFrom:
- configMapRef:
name: plane-config
- secretRef:
name: plane-secrets
Redis 배포
1. Redis Deployment
# redis.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: plane-redis
namespace: plane
spec:
replicas: 1
selector:
matchLabels:
app: plane-redis
template:
metadata:
labels:
app: plane-redis
spec:
containers:
- name: redis
image: redis:7.2-alpine
command:
- redis-server
- --appendonly
- "yes"
- --maxmemory
- "256mb"
- --maxmemory-policy
- "allkeys-lru"
ports:
- containerPort: 6379
name: redis
volumeMounts:
- name: redis-data
mountPath: /data
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "200m"
livenessProbe:
exec:
command:
- redis-cli
- ping
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
exec:
command:
- redis-cli
- ping
initialDelaySeconds: 5
periodSeconds: 5
volumes:
- name: redis-data
persistentVolumeClaim:
claimName: plane-redis-pvc
---
apiVersion: v1
kind: Service
metadata:
name: plane-redis
namespace: plane
spec:
selector:
app: plane-redis
ports:
- port: 6379
targetPort: 6379
name: redis
type: ClusterIP
MinIO 배포
1. MinIO StatefulSet
# minio.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: plane-minio
namespace: plane
spec:
serviceName: plane-minio
replicas: 1
selector:
matchLabels:
app: plane-minio
template:
metadata:
labels:
app: plane-minio
spec:
containers:
- name: minio
image: minio/minio:latest
command:
- /bin/bash
- -c
args:
- minio server /export --console-address ":9090"
env:
- name: MINIO_ROOT_USER
valueFrom:
configMapKeyRef:
name: plane-config
key: MINIO_ROOT_USER
- name: MINIO_ROOT_PASSWORD
valueFrom:
configMapKeyRef:
name: plane-config
key: MINIO_ROOT_PASSWORD
ports:
- containerPort: 9000
name: minio
- containerPort: 9090
name: minio-console
volumeMounts:
- name: minio-data
mountPath: /export
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "200m"
livenessProbe:
httpGet:
path: /minio/health/live
port: 9000
initialDelaySeconds: 30
periodSeconds: 20
readinessProbe:
httpGet:
path: /minio/health/ready
port: 9000
initialDelaySeconds: 10
periodSeconds: 10
volumes:
- name: minio-data
persistentVolumeClaim:
claimName: plane-minio-pvc
---
apiVersion: v1
kind: Service
metadata:
name: plane-minio
namespace: plane
spec:
selector:
app: plane-minio
ports:
- port: 9000
targetPort: 9000
name: minio
- port: 9090
targetPort: 9090
name: minio-console
type: ClusterIP
2. MinIO 버킷 초기화
# minio-init-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: plane-minio-init
namespace: plane
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: minio-init
image: minio/mc:latest
command: ["/bin/sh"]
args:
- -c
- |
until mc alias set plane http://plane-minio:9000 $MINIO_ROOT_USER $MINIO_ROOT_PASSWORD; do
echo "Waiting for MinIO..."
sleep 2
done
mc mb plane/uploads --ignore-existing
mc policy set public plane/uploads
echo "MinIO initialization completed"
env:
- name: MINIO_ROOT_USER
valueFrom:
configMapKeyRef:
name: plane-config
key: MINIO_ROOT_USER
- name: MINIO_ROOT_PASSWORD
valueFrom:
configMapKeyRef:
name: plane-config
key: MINIO_ROOT_PASSWORD
Plane 애플리케이션 배포
1. API 서버 배포
# plane-api.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: plane-api
namespace: plane
labels:
app: plane-api
spec:
replicas: 2
selector:
matchLabels:
app: plane-api
template:
metadata:
labels:
app: plane-api
spec:
initContainers:
- name: wait-for-db
image: busybox:1.35
command: ['sh', '-c']
args:
- |
until nc -z plane-postgresql 5432; do
echo "Waiting for PostgreSQL..."
sleep 2
done
echo "PostgreSQL is ready!"
containers:
- name: plane-api
image: makeplane/plane-backend:latest
ports:
- containerPort: 8000
name: http
env:
- name: DATABASE_URL
value: "postgres://plane:$(POSTGRES_PASSWORD)@plane-postgresql:5432/plane"
- name: REDIS_URL
value: "redis://plane-redis:6379"
envFrom:
- configMapRef:
name: plane-config
- secretRef:
name: plane-secrets
resources:
requests:
memory: "512Mi"
cpu: "200m"
limits:
memory: "1Gi"
cpu: "500m"
livenessProbe:
httpGet:
path: /api/health/
port: 8000
initialDelaySeconds: 60
periodSeconds: 30
timeoutSeconds: 10
readinessProbe:
httpGet:
path: /api/health/
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
volumeMounts:
- name: static-files
mountPath: /app/static
volumes:
- name: static-files
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: plane-api
namespace: plane
spec:
selector:
app: plane-api
ports:
- port: 8000
targetPort: 8000
name: http
type: ClusterIP
2. Worker 서비스 배포
# plane-worker.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: plane-worker
namespace: plane
labels:
app: plane-worker
spec:
replicas: 2
selector:
matchLabels:
app: plane-worker
template:
metadata:
labels:
app: plane-worker
spec:
containers:
- name: plane-worker
image: makeplane/plane-backend:latest
command: ["celery"]
args: ["-A", "plane.settings.celery", "worker", "-l", "info"]
env:
- name: DATABASE_URL
value: "postgres://plane:$(POSTGRES_PASSWORD)@plane-postgresql:5432/plane"
- name: REDIS_URL
value: "redis://plane-redis:6379"
envFrom:
- configMapRef:
name: plane-config
- secretRef:
name: plane-secrets
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "200m"
livenessProbe:
exec:
command:
- /bin/sh
- -c
- "celery -A plane.settings.celery inspect ping"
initialDelaySeconds: 60
periodSeconds: 30
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: plane-beat
namespace: plane
labels:
app: plane-beat
spec:
replicas: 1
selector:
matchLabels:
app: plane-beat
template:
metadata:
labels:
app: plane-beat
spec:
containers:
- name: plane-beat
image: makeplane/plane-backend:latest
command: ["celery"]
args: ["-A", "plane.settings.celery", "beat", "-l", "info"]
env:
- name: DATABASE_URL
value: "postgres://plane:$(POSTGRES_PASSWORD)@plane-postgresql:5432/plane"
- name: REDIS_URL
value: "redis://plane-redis:6379"
envFrom:
- configMapRef:
name: plane-config
- secretRef:
name: plane-secrets
resources:
requests:
memory: "128Mi"
cpu: "50m"
limits:
memory: "256Mi"
cpu: "100m"
3. Web 애플리케이션 배포
# plane-web.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: plane-web
namespace: plane
labels:
app: plane-web
spec:
replicas: 2
selector:
matchLabels:
app: plane-web
template:
metadata:
labels:
app: plane-web
spec:
containers:
- name: plane-web
image: makeplane/plane-frontend:latest
ports:
- containerPort: 3000
name: http
env:
- name: NEXT_PUBLIC_API_BASE_URL
value: "https://plane.yourdomain.com/api"
- name: NEXT_PUBLIC_DEPLOY_URL
value: "https://plane.yourdomain.com"
envFrom:
- configMapRef:
name: plane-config
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "200m"
livenessProbe:
httpGet:
path: /
port: 3000
initialDelaySeconds: 30
periodSeconds: 30
readinessProbe:
httpGet:
path: /
port: 3000
initialDelaySeconds: 10
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: plane-web
namespace: plane
spec:
selector:
app: plane-web
ports:
- port: 3000
targetPort: 3000
name: http
type: ClusterIP
4. Admin 패널 배포
# plane-admin.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: plane-admin
namespace: plane
labels:
app: plane-admin
spec:
replicas: 1
selector:
matchLabels:
app: plane-admin
template:
metadata:
labels:
app: plane-admin
spec:
containers:
- name: plane-admin
image: makeplane/plane-admin:latest
ports:
- containerPort: 3001
name: http
env:
- name: NEXT_PUBLIC_API_BASE_URL
value: "https://plane.yourdomain.com/api"
- name: NEXT_PUBLIC_ADMIN_BASE_URL
value: "https://plane.yourdomain.com/admin"
envFrom:
- configMapRef:
name: plane-config
resources:
requests:
memory: "128Mi"
cpu: "50m"
limits:
memory: "256Mi"
cpu: "100m"
---
apiVersion: v1
kind: Service
metadata:
name: plane-admin
namespace: plane
spec:
selector:
app: plane-admin
ports:
- port: 3001
targetPort: 3001
name: http
type: ClusterIP
Ingress 및 네트워킹 설정
1. NGINX Ingress Controller 설치
# OrbStack에서 NGINX Ingress Controller 설치
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.8.1/deploy/static/provider/cloud/deploy.yaml
# 설치 확인
kubectl get pods -n ingress-nginx
kubectl get svc -n ingress-nginx
2. SSL 인증서 설정 (Let’s Encrypt)
# cert-manager-issuer.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@yourdomain.com
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- http01:
ingress:
class: nginx
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
server: https://acme-staging-v02.api.letsencrypt.org/directory
email: admin@yourdomain.com
privateKeySecretRef:
name: letsencrypt-staging
solvers:
- http01:
ingress:
class: nginx
3. Ingress 리소스 구성
# plane-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: plane-ingress
namespace: plane
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
nginx.ingress.kubernetes.io/proxy-send-timeout: "300"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/configuration-snippet: |
more_set_headers "X-Frame-Options: SAMEORIGIN";
more_set_headers "X-Content-Type-Options: nosniff";
more_set_headers "X-XSS-Protection: 1; mode=block";
spec:
ingressClassName: nginx
tls:
- hosts:
- plane.yourdomain.com
secretName: plane-tls
rules:
- host: plane.yourdomain.com
http:
paths:
# API 라우팅
- path: /api
pathType: Prefix
backend:
service:
name: plane-api
port:
number: 8000
# Admin 패널 라우팅
- path: /admin
pathType: Prefix
backend:
service:
name: plane-admin
port:
number: 3001
# MinIO 파일 업로드 라우팅
- path: /uploads
pathType: Prefix
backend:
service:
name: plane-minio
port:
number: 9000
# 메인 웹 애플리케이션 (기본값)
- path: /
pathType: Prefix
backend:
service:
name: plane-web
port:
number: 3000
4. 네트워크 정책 설정
# network-policy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: plane-network-policy
namespace: plane
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
ingress:
# Ingress Controller로부터의 트래픽 허용
- from:
- namespaceSelector:
matchLabels:
name: ingress-nginx
# 네임스페이스 내부 통신 허용
- from:
- namespaceSelector:
matchLabels:
name: plane
egress:
# DNS 해결을 위한 kube-system 접근
- to:
- namespaceSelector:
matchLabels:
name: kube-system
ports:
- protocol: UDP
port: 53
# 네임스페이스 내부 통신 허용
- to:
- namespaceSelector:
matchLabels:
name: plane
# 외부 서비스 접근 (GitHub, Slack 등)
- to: []
ports:
- protocol: TCP
port: 443
- protocol: TCP
port: 80
OrbStack에서 테스트
1. 전체 배포 스크립트
#!/bin/bash
# deploy-plane-orbstack.sh
set -e
echo "🚀 Plane 쿠버네티스 배포 시작..."
# 네임스페이스 생성
kubectl apply -f namespace.yaml
# Secret 생성
echo "🔐 Secret 생성 중..."
chmod +x create-secrets.sh
./create-secrets.sh
# ConfigMap 및 PVC 생성
kubectl apply -f configmap.yaml
kubectl apply -f storage.yaml
# 데이터베이스 및 캐시 배포
echo "🗄️ 데이터베이스 서비스 배포 중..."
kubectl apply -f postgresql.yaml
kubectl apply -f redis.yaml
kubectl apply -f minio.yaml
# 데이터베이스 초기화 대기
echo "⏳ 데이터베이스 초기화 대기 중..."
kubectl wait --for=condition=available --timeout=300s deployment/plane-redis -n plane
kubectl wait --for=condition=ready --timeout=300s pod -l app=plane-postgresql -n plane
# DB 마이그레이션 실행
echo "📊 데이터베이스 마이그레이션 실행 중..."
kubectl apply -f postgresql-init-job.yaml
kubectl apply -f minio-init-job.yaml
# 마이그레이션 완료 대기
kubectl wait --for=condition=complete --timeout=300s job/plane-db-migration -n plane
kubectl wait --for=condition=complete --timeout=300s job/plane-minio-init -n plane
# 애플리케이션 배포
echo "🎯 Plane 애플리케이션 배포 중..."
kubectl apply -f plane-api.yaml
kubectl apply -f plane-worker.yaml
kubectl apply -f plane-web.yaml
kubectl apply -f plane-admin.yaml
# 애플리케이션 준비 대기
echo "⏳ 애플리케이션 시작 대기 중..."
kubectl wait --for=condition=available --timeout=300s deployment/plane-api -n plane
kubectl wait --for=condition=available --timeout=300s deployment/plane-web -n plane
# Ingress 설정
echo "🌐 Ingress 설정 중..."
kubectl apply -f plane-ingress.yaml
echo "✅ 배포 완료!"
echo ""
echo "📋 배포 상태 확인:"
kubectl get pods -n plane
echo ""
echo "🌐 서비스 접근:"
echo " Web: http://localhost"
echo " API: http://localhost/api"
echo " Admin: http://localhost/admin"
echo ""
echo "🔍 로그 확인:"
echo " kubectl logs -f deployment/plane-api -n plane"
echo " kubectl logs -f deployment/plane-web -n plane"
2. OrbStack 전용 설정
# orbstack-loadbalancer.yaml
apiVersion: v1
kind: Service
metadata:
name: plane-loadbalancer
namespace: plane
annotations:
# OrbStack LoadBalancer 설정
service.beta.kubernetes.io/aws-load-balancer-type: "nlb"
spec:
type: LoadBalancer
selector:
app: plane-web
ports:
- port: 80
targetPort: 3000
name: http
- port: 8000
targetPort: 8000
name: api
---
# OrbStack 개발용 간단한 Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: plane-dev-ingress
namespace: plane
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: plane-api
port:
number: 8000
- path: /admin
pathType: Prefix
backend:
service:
name: plane-admin
port:
number: 3001
- path: /
pathType: Prefix
backend:
service:
name: plane-web
port:
number: 3000
3. 개발 환경 테스트 스크립트
#!/bin/bash
# test-plane-orbstack.sh
echo "🧪 Plane OrbStack 테스트 시작..."
# 포드 상태 확인
echo "📊 포드 상태:"
kubectl get pods -n plane -o wide
# 서비스 상태 확인
echo -e "\n🌐 서비스 상태:"
kubectl get svc -n plane
# Ingress 상태 확인
echo -e "\n🚪 Ingress 상태:"
kubectl get ingress -n plane
# API 헬스체크
echo -e "\n🏥 API 헬스체크:"
API_POD=$(kubectl get pods -n plane -l app=plane-api -o jsonpath='{.items[0].metadata.name}')
kubectl exec -n plane $API_POD -- curl -s http://localhost:8000/api/health/ || echo "API 헬스체크 실패"
# 웹 애플리케이션 접근 테스트
echo -e "\n🌍 웹 애플리케이션 테스트:"
WEB_POD=$(kubectl get pods -n plane -l app=plane-web -o jsonpath='{.items[0].metadata.name}')
kubectl exec -n plane $WEB_POD -- curl -s -o /dev/null -w "%{http_code}" http://localhost:3000 || echo "웹 애플리케이션 접근 실패"
# 데이터베이스 연결 테스트
echo -e "\n🗄️ 데이터베이스 연결 테스트:"
DB_POD=$(kubectl get pods -n plane -l app=plane-postgresql -o jsonpath='{.items[0].metadata.name}')
kubectl exec -n plane $DB_POD -- pg_isready -U plane && echo "데이터베이스 연결 성공" || echo "데이터베이스 연결 실패"
# Redis 연결 테스트
echo -e "\n🚀 Redis 연결 테스트:"
REDIS_POD=$(kubectl get pods -n plane -l app=plane-redis -o jsonPath='{.items[0].metadata.name}')
kubectl exec -n plane $REDIS_POD -- redis-cli ping && echo "Redis 연결 성공" || echo "Redis 연결 실패"
# MinIO 접근 테스트
echo -e "\n📦 MinIO 접근 테스트:"
MINIO_POD=$(kubectl get pods -n plane -l app=plane-minio -o jsonpath='{.items[0].metadata.name}')
kubectl exec -n plane $MINIO_POD -- curl -s -o /dev/null -w "%{http_code}" http://localhost:9000/minio/health/live || echo "MinIO 접근 실패"
# 로그 확인
echo -e "\n📜 최근 로그 (마지막 10줄):"
echo "API 로그:"
kubectl logs --tail=10 -n plane deployment/plane-api
echo -e "\nWeb 로그:"
kubectl logs --tail=10 -n plane deployment/plane-web
echo -e "\n✅ 테스트 완료!"
# 접근 URL 출력
INGRESS_IP=$(kubectl get svc -n ingress-nginx ingress-nginx-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
if [ -n "$INGRESS_IP" ]; then
echo "🌐 접근 가능한 URL:"
echo " Web: http://$INGRESS_IP"
echo " API: http://$INGRESS_IP/api"
echo " Admin: http://$INGRESS_IP/admin"
else
echo "🌐 로컬 접근:"
echo " kubectl port-forward -n plane svc/plane-web 3000:3000"
echo " kubectl port-forward -n plane svc/plane-api 8000:8000"
fi
4. 개발 편의 도구
#!/bin/bash
# plane-dev-tools.sh
# 빠른 포트 포워딩
plane_port_forward() {
echo "🚀 포트 포워딩 시작..."
# 백그라운드에서 실행
kubectl port-forward -n plane svc/plane-web 3000:3000 &
kubectl port-forward -n plane svc/plane-api 8000:8000 &
kubectl port-forward -n plane svc/plane-admin 3001:3001 &
kubectl port-forward -n plane svc/plane-minio 9090:9090 &
echo "✅ 포트 포워딩 완료:"
echo " Web: http://localhost:3000"
echo " API: http://localhost:8000"
echo " Admin: http://localhost:3001"
echo " MinIO Console: http://localhost:9090"
# 포트 포워딩 중지를 위한 PID 저장
jobs -p > /tmp/plane-port-forward.pids
echo "중지하려면: kill \$(cat /tmp/plane-port-forward.pids)"
}
# 로그 스트리밍
plane_logs() {
local service=${1:-api}
echo "📜 $service 로그 스트리밍..."
kubectl logs -f -n plane deployment/plane-$service
}
# 리소스 사용량 모니터링
plane_monitor() {
watch kubectl top pods -n plane
}
# 데이터베이스 접속
plane_db() {
local DB_POD=$(kubectl get pods -n plane -l app=plane-postgresql -o jsonpath='{.items[0].metadata.name}')
kubectl exec -it -n plane $DB_POD -- psql -U plane -d plane
}
# Redis CLI 접속
plane_redis() {
local REDIS_POD=$(kubectl get pods -n plane -l app=plane-redis -o jsonpath='{.items[0].metadata.name}')
kubectl exec -it -n plane $REDIS_POD -- redis-cli
}
# 사용법 출력
case "$1" in
port-forward|pf)
plane_port_forward
;;
logs)
plane_logs $2
;;
monitor|top)
plane_monitor
;;
db)
plane_db
;;
redis)
plane_redis
;;
*)
echo "Plane 개발 도구 사용법:"
echo " $0 port-forward # 포트 포워딩 시작"
echo " $0 logs [api|web|worker] # 로그 스트리밍"
echo " $0 monitor # 리소스 모니터링"
echo " $0 db # 데이터베이스 접속"
echo " $0 redis # Redis CLI 접속"
;;
esac