# OKE(Oracle Kubernetes Engine) 배포 가이드 > Spring Boot + Next.js 앱을 OKE에 배포하는 전체 과정을 정리한 문서. > Colima(로컬 ARM64 Docker) → OCIR(이미지 레지스트리) → OKE(K8s 클러스터) 파이프라인 기준. --- ## 목차 1. [사전 준비](#1-사전-준비) 2. [인프라 아키텍처](#2-인프라-아키텍처) 3. [최초 클러스터 설정 (1회성)](#3-최초-클러스터-설정-1회성) 4. [K8s 매니페스트 구조](#4-k8s-매니페스트-구조) 5. [Dockerfile 작성](#5-dockerfile-작성) 6. [배포 스크립트 (deploy.sh)](#6-배포-스크립트-deploysh) 7. [일상적인 배포 절차](#7-일상적인-배포-절차) 8. [환경별 설정 관리](#8-환경별-설정-관리) 9. [도메인 및 SSL 설정](#9-도메인-및-ssl-설정) 10. [트러블슈팅](#10-트러블슈팅) 11. [유용한 kubectl 명령어](#11-유용한-kubectl-명령어) --- ## 1. 사전 준비 ### 필요한 도구 | 도구 | 용도 | 설치 | |------|------|------| | **OCI CLI** | OKE 인증, kubeconfig 설정 | `brew install oci-cli` | | **kubectl** | K8s 클러스터 관리 | `brew install kubectl` | | **Colima** | 로컬 ARM64 Docker 빌드 (Docker Desktop 대체) | `brew install colima` | | **Docker CLI** | 이미지 빌드/푸시 | `brew install docker` | | **Helm** | Nginx Ingress, cert-manager 설치 | `brew install helm` | ### OCI 설정 ```bash # OCI CLI 프로파일 설정 (~/.oci/config) [DEFAULT] user=ocid1.user.oc1..xxxx fingerprint=xx:xx:xx:... tenancy=ocid1.tenancy.oc1..xxxx region=ap-seoul-1 key_file=~/.oci/oci_api_key.pem ``` ### kubeconfig 설정 ```bash # OKE 클러스터 접근 설정 oci ce cluster create-kubeconfig \ --cluster-id ocid1.cluster.oc1.. \ --file $HOME/.kube/config \ --region \ --token-version 2.0.0 \ --kube-endpoint PUBLIC_ENDPOINT # 연결 확인 kubectl get nodes ``` ### OCIR(Oracle Container Image Registry) 로그인 ```bash # OCIR 로그인 # 사용자명 형식: /oracleidentitycloudservice/ # 비밀번호: OCI Console → User Settings → Auth Tokens에서 발급 docker login .ocir.io # 예) docker login icn.ocir.io ``` ### Colima 시작 (ARM64 빌드용) ```bash # OKE 노드가 ARM64인 경우 반드시 ARM64로 빌드해야 함 colima start --arch aarch64 --cpu 4 --memory 4 # ⚠️ Colima 시작 시 kubectl 컨텍스트가 'colima'로 바뀜 # OKE 컨텍스트로 복원 필수 kubectl config use-context ``` --- ## 2. 인프라 아키텍처 ``` ┌─────────────────────────────────────────────────────┐ │ Internet (사용자) │ │ https://www.example.com │ └────────────────┬────────────────────────────────────┘ │ ┌───────▼────────┐ │ DNS Provider │ ← A 레코드 → NLB Public IP └───────┬────────┘ │ ┌────────────▼──────────────┐ │ OCI Network Load Balancer │ ← Nginx Ingress가 자동 생성 └────────────┬──────────────┘ │ ┌────────────▼──────────────────────────────────┐ │ OKE Cluster │ │ │ │ ┌─────────────────────────────┐ │ │ │ Nginx Ingress Controller │ │ │ │ + cert-manager (Let's Encrypt)│ │ │ └───┬─────────────┬───────────┘ │ │ │ │ │ │ /api/* /* │ │ │ │ │ │ ┌───▼──────┐ ┌──▼───────┐ ┌──────────┐ │ │ │ Backend │ │ Frontend │ │ Redis │ │ │ │ :8000 │ │ :3001 │ │ :6379 │ │ │ └──────────┘ └──────────┘ └──────────┘ │ └───────────────────────────────────────────────┘ │ ┌────────────▼──────────────────────┐ │ External Services │ │ ├─ Oracle ADB (mTLS + Wallet) │ │ ├─ OCI GenAI │ │ └─ 기타 외부 API │ └────────────────────────────────────┘ ``` ### 리소스 할당 예시 (ARM64 × 2노드, 2CPU/8GB 각) | 컴포넌트 | CPU request/limit | Memory request/limit | |----------|-------------------|----------------------| | Backend | 500m / 1 | 768Mi / 1536Mi | | Frontend | 200m / 500m | 256Mi / 512Mi | | Redis | 100m / 200m | 128Mi / 256Mi | | **합계** | **~800m** | **~1.2GB** | --- ## 3. 최초 클러스터 설정 (1회성) ### 3.1 Nginx Ingress Controller 설치 ```bash helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx helm repo update helm install ingress-nginx ingress-nginx/ingress-nginx \ --namespace ingress-nginx \ --create-namespace \ --set controller.service.type=LoadBalancer \ --set controller.service.annotations."oci\.oraclecloud\.com/load-balancer-type"="nlb" \ --set controller.service.externalTrafficPolicy=Local # NLB 외부 IP 확인 (DNS에 연결할 IP) kubectl get svc -n ingress-nginx ingress-nginx-controller ``` > **OKE 네트워크 설정**: VCN Security List에서 NodePort 범위(30000~32767)를 NLB IP 대역에서 허용해야 함. ### 3.2 cert-manager 설치 (Let's Encrypt 자동 인증서) ```bash helm repo add jetstack https://charts.jetstack.io helm repo update helm install cert-manager jetstack/cert-manager \ --namespace cert-manager \ --create-namespace \ --set crds.enabled=true ``` ### 3.3 ClusterIssuer 생성 ```yaml # k8s/cert-manager/cluster-issuer.yaml apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: letsencrypt-prod spec: acme: server: https://acme-v02.api.letsencrypt.org/directory email: your-email@example.com privateKeySecretRef: name: letsencrypt-prod solvers: - http01: ingress: class: nginx ``` ```bash kubectl apply -f k8s/cert-manager/cluster-issuer.yaml ``` ### 3.4 앱 네임스페이스 및 시크릿 생성 ```bash # 네임스페이스 kubectl apply -f k8s/namespace.yaml # OCIR pull secret (이미지 다운로드용) kubectl create secret docker-registry ocir-secret \ --namespace \ --docker-server=.ocir.io \ --docker-username='/oracleidentitycloudservice/' \ --docker-password='' # Oracle Wallet (DB mTLS 인증) kubectl create secret generic oracle-wallet \ --namespace \ --from-file=cwallet.sso \ --from-file=ewallet.p12 \ --from-file=tnsnames.ora \ --from-file=sqlnet.ora \ --from-file=ojdbc.properties \ --from-file=keystore.jks \ --from-file=truststore.jks # OCI 설정 (GenAI 등 OCI SDK 인증) kubectl create secret generic oci-config \ --namespace \ --from-file=config=~/.oci/config \ --from-file=oci_api_key.pem=~/.oci/oci_api_key.pem # 앱 시크릿/설정 kubectl apply -f k8s/secrets.yaml kubectl apply -f k8s/configmap.yaml ``` ### 3.5 앱 배포 (최초) ```bash kubectl apply -f k8s/redis-deployment.yaml kubectl apply -f k8s/backend-deployment.yaml kubectl apply -f k8s/frontend-deployment.yaml kubectl apply -f k8s/ingress.yaml # 롤아웃 확인 kubectl rollout status deployment/backend -n kubectl rollout status deployment/frontend -n ``` --- ## 4. K8s 매니페스트 구조 ``` k8s/ ├── namespace.yaml # 네임스페이스 정의 ├── configmap.yaml # 비민감 설정 (Redis 호스트, API 엔드포인트 등) ├── secrets.yaml # 민감 정보 (DB 비밀번호, API 키 등) ← .gitignore ├── secrets.yaml.template # 시크릿 템플릿 (Git에 포함) ├── backend-deployment.yaml # Backend Deployment + Service ├── frontend-deployment.yaml # Frontend Deployment + Service ├── redis-deployment.yaml # Redis Deployment + Service ├── ingress.yaml # Ingress (라우팅 + TLS) └── cert-manager/ └── cluster-issuer.yaml # Let's Encrypt ClusterIssuer ``` ### Backend Deployment 예시 ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: backend namespace: spec: replicas: 1 selector: matchLabels: app: backend template: metadata: labels: app: backend spec: imagePullSecrets: - name: ocir-secret containers: - name: backend image: .ocir.io///backend:latest ports: - containerPort: 8000 envFrom: - configMapRef: name: -config - secretRef: name: -secrets volumeMounts: - name: oracle-wallet mountPath: /etc/oracle/wallet readOnly: true - name: oci-config mountPath: /root/.oci readOnly: true resources: requests: cpu: "500m" memory: "768Mi" limits: cpu: "1" memory: "1536Mi" readinessProbe: tcpSocket: port: 8000 initialDelaySeconds: 30 periodSeconds: 10 livenessProbe: tcpSocket: port: 8000 initialDelaySeconds: 60 periodSeconds: 30 volumes: - name: oracle-wallet secret: secretName: oracle-wallet - name: oci-config secret: secretName: oci-config --- apiVersion: v1 kind: Service metadata: name: backend namespace: spec: selector: app: backend ports: - port: 8000 targetPort: 8000 ``` ### Ingress 예시 ```yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: -ingress namespace: annotations: cert-manager.io/cluster-issuer: letsencrypt-prod nginx.ingress.kubernetes.io/ssl-redirect: "true" nginx.ingress.kubernetes.io/proxy-read-timeout: "300" nginx.ingress.kubernetes.io/proxy-send-timeout: "300" nginx.ingress.kubernetes.io/proxy-body-size: "10m" spec: ingressClassName: nginx tls: - hosts: - www.example.com secretName: -tls rules: - host: www.example.com http: paths: - path: /api pathType: Prefix backend: service: name: backend port: number: 8000 - path: / pathType: Prefix backend: service: name: frontend port: number: 3001 ``` --- ## 5. Dockerfile 작성 ### Backend (Spring Boot 멀티스테이지) ```dockerfile # ── Build Stage ── FROM eclipse-temurin:21-jdk AS build WORKDIR /app # 의존성 캐싱 (소스 변경 시에도 재사용) COPY gradlew settings.gradle build.gradle ./ COPY gradle/ gradle/ RUN chmod +x gradlew && ./gradlew dependencies --no-daemon || true # 소스 복사 & 빌드 COPY src/ src/ RUN ./gradlew bootJar -x test --no-daemon # ── Runtime Stage ── FROM eclipse-temurin:21-jre WORKDIR /app COPY --from=build /app/build/libs/*.jar app.jar EXPOSE 8000 ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0 -XX:+UseG1GC" ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"] ``` **포인트:** - Gradle 의존성을 먼저 복사/다운로드 → Docker 레이어 캐시 활용 - `-XX:MaxRAMPercentage=75.0` → 컨테이너 메모리 제한의 75%를 JVM 힙으로 사용 - 최종 이미지에 JDK 대신 JRE만 포함 → 이미지 크기 절감 ### Frontend (Next.js standalone 멀티스테이지) ```dockerfile # ── Build Stage ── FROM node:22-alpine AS build WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci COPY . . # 빌드 시점 환경변수 주입 (NEXT_PUBLIC_* 변수) ARG NEXT_PUBLIC_GOOGLE_MAPS_API_KEY ARG NEXT_PUBLIC_GOOGLE_CLIENT_ID RUN npm run build # ── Runtime Stage ── FROM node:22-alpine WORKDIR /app # standalone 출력 + 정적 파일 + public 복사 COPY --from=build /app/.next/standalone ./ COPY --from=build /app/.next/static ./.next/static COPY --from=build /app/public ./public EXPOSE 3001 ENV PORT=3001 HOSTNAME=0.0.0.0 CMD ["node", "server.js"] ``` **포인트:** - `next.config.ts`에 `output: "standalone"` 필수 - `NEXT_PUBLIC_*` 환경변수는 **빌드 시점**에만 주입 가능 (런타임 X) - `.next/static`과 `public/`은 standalone에 포함되지 않으므로 수동 복사 --- ## 6. 배포 스크립트 (deploy.sh) ```bash #!/bin/bash set -euo pipefail # ── Configuration ── REGISTRY=".ocir.io//" APP_NAMESPACE="" PLATFORM="linux/arm64" # OKE 노드 아키텍처에 맞춤 # ── Parse arguments ── TARGET="all" # all | backend | frontend MESSAGE="" while [[ $# -gt 0 ]]; do case $1 in --backend-only) TARGET="backend"; shift ;; --frontend-only) TARGET="frontend"; shift ;; --dry-run) echo "[DRY RUN]"; exit 0 ;; -m) MESSAGE="$2"; shift 2 ;; *) MESSAGE="$1"; shift ;; esac done # ── Auto-increment version ── LATEST_TAG=$(git tag --sort=-v:refname | grep '^v' | head -1 2>/dev/null || echo "v0.1.0") MAJOR=$(echo "$LATEST_TAG" | cut -d. -f1) MINOR=$(echo "$LATEST_TAG" | cut -d. -f2) PATCH=$(echo "$LATEST_TAG" | cut -d. -f3) TAG="${MAJOR}.${MINOR}.$((PATCH + 1))" echo "━━━ Deploying ${TAG} (${TARGET}) ━━━" cd "$(git rev-parse --show-toplevel)" # ── Build & Push ── if [[ "$TARGET" == "all" || "$TARGET" == "backend" ]]; then echo "▶ Building backend..." docker build --platform "$PLATFORM" \ -t "$REGISTRY/backend:$TAG" \ -t "$REGISTRY/backend:latest" \ backend-java/ docker push "$REGISTRY/backend:$TAG" docker push "$REGISTRY/backend:latest" fi if [[ "$TARGET" == "all" || "$TARGET" == "frontend" ]]; then echo "▶ Building frontend..." # .env.local에서 빌드 인자 읽기 MAPS_KEY=$(grep NEXT_PUBLIC_GOOGLE_MAPS_API_KEY frontend/.env.local 2>/dev/null | cut -d= -f2) CLIENT_ID=$(grep NEXT_PUBLIC_GOOGLE_CLIENT_ID frontend/.env.local 2>/dev/null | cut -d= -f2) docker build --platform "$PLATFORM" \ --build-arg NEXT_PUBLIC_GOOGLE_MAPS_API_KEY="$MAPS_KEY" \ --build-arg NEXT_PUBLIC_GOOGLE_CLIENT_ID="$CLIENT_ID" \ -t "$REGISTRY/frontend:$TAG" \ -t "$REGISTRY/frontend:latest" \ frontend/ docker push "$REGISTRY/frontend:$TAG" docker push "$REGISTRY/frontend:latest" fi # ── Rolling Update ── # ⚠️ kubectl 컨텍스트가 OKE를 가리키는지 반드시 확인 if [[ "$TARGET" == "all" || "$TARGET" == "backend" ]]; then kubectl set image deployment/backend \ backend="$REGISTRY/backend:$TAG" -n "$APP_NAMESPACE" kubectl rollout status deployment/backend -n "$APP_NAMESPACE" --timeout=180s fi if [[ "$TARGET" == "all" || "$TARGET" == "frontend" ]]; then kubectl set image deployment/frontend \ frontend="$REGISTRY/frontend:$TAG" -n "$APP_NAMESPACE" kubectl rollout status deployment/frontend -n "$APP_NAMESPACE" --timeout=120s fi # ── Git Tag ── git tag -a "$TAG" -m "Deploy ${TAG}: ${MESSAGE}" git push origin "$TAG" echo "━━━ ✅ Deploy complete: ${TAG} ━━━" kubectl get pods -n "$APP_NAMESPACE" ``` --- ## 7. 일상적인 배포 절차 ```bash # 1. Colima 시작 (ARM64 Docker 빌드용, 꺼져있을 때만) colima start --arch aarch64 --cpu 4 --memory 4 # 2. kubectl 컨텍스트를 OKE로 복원 (Colima가 바꿔버림) kubectl config use-context # 3. 배포 실행 ./deploy.sh "변경 내용 설명" # 전체 배포 ./deploy.sh --backend-only "API 수정" # 백엔드만 ./deploy.sh --frontend-only "UI 수정" # 프론트엔드만 # 4. 확인 kubectl get pods -n kubectl logs -f deployment/backend -n # 5. (선택) Colima 중지 (리소스 절약) colima stop ``` ### 시크릿/설정만 변경할 때 ```bash # secrets.yaml 수정 후 kubectl apply -f k8s/secrets.yaml # 시크릿 변경은 Pod를 자동 재시작하지 않음 → 수동 재시작 필요 kubectl rollout restart deployment/backend -n ``` ### 롤백 ```bash # 이전 버전으로 롤백 kubectl rollout undo deployment/backend -n kubectl rollout undo deployment/frontend -n # 특정 리비전으로 롤백 kubectl rollout history deployment/backend -n kubectl rollout undo deployment/backend -n --to-revision=3 ``` --- ## 8. 환경별 설정 관리 ### 개발(Local) vs 운영(OKE) 비교 | 항목 | 개발 (Local) | 운영 (OKE) | |------|-------------|-----------| | DB 설정 | `backend/.env` | K8s Secret (`secrets.yaml`) | | 환경변수 | 파일 기반 (`.env`, `.env.local`) | ConfigMap + Secret | | Oracle Wallet | 로컬 디렉토리 | K8s Secret → Volume 마운트 | | OCI 인증 | `~/.oci/config` | K8s Secret → Volume 마운트 | | Redis | 로컬 IP (`192.168.x.x`) | K8s Service DNS (`redis`) | | 프로세스 관리 | PM2 | K8s Deployment | | 프론트엔드 모드 | `npm run dev` | `node server.js` (standalone) | | DB 프로파일 | `_low` (리소스 절약) | `_medium` (적정 성능) | ### Oracle ADB 프로파일 선택 | 프로파일 | 병렬 처리 | 용도 | |---------|----------|------| | `_high` | 최대 | 대규모 배치/분석 | | `_medium` | 중간 | **운영 권장** | | `_low` | 최소 | **개발/테스트** (OCPU 절약) | | `_tp` | 트랜잭션 | OLTP 워크로드 | --- ## 9. 도메인 및 SSL 설정 ### DNS 설정 ``` # DNS Provider (예: Namecheap)에서 설정 Type Host Value TTL A @ 300 A www 300 ``` NLB Public IP 확인: ```bash kubectl get svc -n ingress-nginx ingress-nginx-controller \ -o jsonpath='{.status.loadBalancer.ingress[0].ip}' ``` ### SSL 인증서 cert-manager + ClusterIssuer가 설정되어 있으면 Ingress에 annotation만 추가하면 자동 발급/갱신됨. ```yaml # Ingress에 추가 metadata: annotations: cert-manager.io/cluster-issuer: letsencrypt-prod spec: tls: - hosts: - www.example.com secretName: -tls # cert-manager가 자동 생성 ``` 인증서 상태 확인: ```bash kubectl get certificate -n kubectl describe certificate -tls -n ``` --- ## 10. 트러블슈팅 ### 이미지 Pull 실패 (ImagePullBackOff) ```bash kubectl describe pod -n # Events 섹션에서 에러 확인 # 원인 1: OCIR 인증 실패 → pull secret 재생성 kubectl delete secret ocir-secret -n kubectl create secret docker-registry ocir-secret \ --namespace \ --docker-server=.ocir.io \ --docker-username='/oracleidentitycloudservice/' \ --docker-password='' # 원인 2: 이미지가 존재하지 않음 → OCIR 콘솔에서 확인 ``` ### OKE CRI-O: short image name 오류 ``` # ❌ 틀림 image: redis:7-alpine # ✅ 맞음 (docker.io/library/ 접두사 필수) image: docker.io/library/redis:7-alpine ``` OKE는 CRI-O 런타임을 사용하며, Docker Hub의 `library/` 이미지도 전체 경로를 명시해야 함. ### DB 연결 실패 (HikariPool timeout) ```bash # Pod 로그 확인 kubectl logs deployment/backend -n | grep -i "hikari\|oracle\|connection" # 원인 1: Wallet이 마운트되지 않음 kubectl exec deployment/backend -n -- ls /etc/oracle/wallet/ # 원인 2: ORACLE_DSN에 TNS_ADMIN 경로가 잘못됨 # 형식: _medium?TNS_ADMIN=/etc/oracle/wallet ``` ### Let's Encrypt 인증서 발급 실패 ```bash # Challenge 상태 확인 kubectl get challenges -n kubectl describe challenge -n # 원인: VCN Security List에서 80번 포트가 막혀있음 # OCI Console → VCN → Security Lists → Ingress Rules에 HTTP 80 허용 추가 ``` ### kubectl 인증 실패 ```bash # kubeconfig 재생성 oci ce cluster create-kubeconfig \ --cluster-id \ --file $HOME/.kube/config \ --region \ --token-version 2.0.0 \ --kube-endpoint PUBLIC_ENDPOINT # OCI CLI 프로파일 지정 (여러 프로파일 사용 시) # kubeconfig의 args에 --profile 추가 ``` ### Colima 시작 후 kubectl 안 됨 ```bash # Colima가 kubectl 컨텍스트를 가져감 kubectl config get-contexts kubectl config use-context # OKE로 복원 ``` --- ## 11. 유용한 kubectl 명령어 ```bash # ── 상태 확인 ── kubectl get pods -n # Pod 목록 kubectl get pods -n -w # 실시간 감시 kubectl get svc -n # Service 목록 kubectl get ingress -n # Ingress 확인 kubectl top pods -n # 리소스 사용량 # ── 로그 ── kubectl logs deployment/backend -n # 최근 로그 kubectl logs deployment/backend -n -f # 실시간 로그 kubectl logs deployment/backend -n --previous # 이전 Pod 로그 (크래시 시) # ── 디버깅 ── kubectl describe pod -n # Pod 상세 (이벤트 포함) kubectl exec -it deployment/backend -n -- sh # Pod 접속 kubectl port-forward svc/backend 8000:8000 -n # 로컬 포트 포워딩 # ── 배포 관리 ── kubectl rollout status deployment/backend -n # 롤아웃 상태 kubectl rollout restart deployment/backend -n # 재시작 (설정 변경 반영) kubectl rollout undo deployment/backend -n # 이전 버전 롤백 kubectl rollout history deployment/backend -n # 배포 이력 # ── 설정 변경 ── kubectl apply -f k8s/configmap.yaml # ConfigMap 업데이트 kubectl apply -f k8s/secrets.yaml # Secret 업데이트 kubectl edit deployment backend -n # 직접 수정 (비추천) # ── 정리 ── kubectl delete pod -n # Pod 강제 재시작 kubectl scale deployment/backend --replicas=0 -n # 일시 중지 kubectl scale deployment/backend --replicas=1 -n # 복원 ```