Files
tasteby/docs/oke-deploy-howto.md
joungmin 21eb1e9562 docs(deploy): OKE 배포 가이드 추가
- Colima(로컬 ARM64 Docker) → OCIR → OKE 파이프라인 전체 절차
- deploy.sh 사용법, kubectl context, secret 등록 흐름

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 19:34:58 +09:00

23 KiB
Raw Permalink Blame History

OKE(Oracle Kubernetes Engine) 배포 가이드

Spring Boot + Next.js 앱을 OKE에 배포하는 전체 과정을 정리한 문서. Colima(로컬 ARM64 Docker) → OCIR(이미지 레지스트리) → OKE(K8s 클러스터) 파이프라인 기준.


목차

  1. 사전 준비
  2. 인프라 아키텍처
  3. 최초 클러스터 설정 (1회성)
  4. K8s 매니페스트 구조
  5. Dockerfile 작성
  6. 배포 스크립트 (deploy.sh)
  7. 일상적인 배포 절차
  8. 환경별 설정 관리
  9. 도메인 및 SSL 설정
  10. 트러블슈팅
  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 설정

# 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 설정

# OKE 클러스터 접근 설정
oci ce cluster create-kubeconfig \
  --cluster-id ocid1.cluster.oc1.<region>.<cluster-id> \
  --file $HOME/.kube/config \
  --region <region> \
  --token-version 2.0.0 \
  --kube-endpoint PUBLIC_ENDPOINT

# 연결 확인
kubectl get nodes

OCIR(Oracle Container Image Registry) 로그인

# OCIR 로그인
# 사용자명 형식: <namespace>/oracleidentitycloudservice/<email>
# 비밀번호: OCI Console → User Settings → Auth Tokens에서 발급

docker login <region-code>.ocir.io
# 예) docker login icn.ocir.io

Colima 시작 (ARM64 빌드용)

# OKE 노드가 ARM64인 경우 반드시 ARM64로 빌드해야 함
colima start --arch aarch64 --cpu 4 --memory 4

# ⚠️ Colima 시작 시 kubectl 컨텍스트가 'colima'로 바뀜
# OKE 컨텍스트로 복원 필수
kubectl config use-context <oke-context-name>

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 설치

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 자동 인증서)

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 생성

# 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
kubectl apply -f k8s/cert-manager/cluster-issuer.yaml

3.4 앱 네임스페이스 및 시크릿 생성

# 네임스페이스
kubectl apply -f k8s/namespace.yaml

# OCIR pull secret (이미지 다운로드용)
kubectl create secret docker-registry ocir-secret \
  --namespace <app-namespace> \
  --docker-server=<region>.ocir.io \
  --docker-username='<namespace>/oracleidentitycloudservice/<email>' \
  --docker-password='<auth-token>'

# Oracle Wallet (DB mTLS 인증)
kubectl create secret generic oracle-wallet \
  --namespace <app-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 <app-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 앱 배포 (최초)

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 <app-namespace>
kubectl rollout status deployment/frontend -n <app-namespace>

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 예시

apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend
  namespace: <app-namespace>
spec:
  replicas: 1
  selector:
    matchLabels:
      app: backend
  template:
    metadata:
      labels:
        app: backend
    spec:
      imagePullSecrets:
        - name: ocir-secret
      containers:
        - name: backend
          image: <region>.ocir.io/<namespace>/<repo>/backend:latest
          ports:
            - containerPort: 8000
          envFrom:
            - configMapRef:
                name: <app>-config
            - secretRef:
                name: <app>-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: <app-namespace>
spec:
  selector:
    app: backend
  ports:
    - port: 8000
      targetPort: 8000

Ingress 예시

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: <app>-ingress
  namespace: <app-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: <app>-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 멀티스테이지)

# ── 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 멀티스테이지)

# ── 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.tsoutput: "standalone" 필수
  • NEXT_PUBLIC_* 환경변수는 빌드 시점에만 주입 가능 (런타임 X)
  • .next/staticpublic/은 standalone에 포함되지 않으므로 수동 복사

6. 배포 스크립트 (deploy.sh)

#!/bin/bash
set -euo pipefail

# ── Configuration ──
REGISTRY="<region>.ocir.io/<namespace>/<repo>"
APP_NAMESPACE="<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. 일상적인 배포 절차

# 1. Colima 시작 (ARM64 Docker 빌드용, 꺼져있을 때만)
colima start --arch aarch64 --cpu 4 --memory 4

# 2. kubectl 컨텍스트를 OKE로 복원 (Colima가 바꿔버림)
kubectl config use-context <oke-context-name>

# 3. 배포 실행
./deploy.sh "변경 내용 설명"              # 전체 배포
./deploy.sh --backend-only "API 수정"     # 백엔드만
./deploy.sh --frontend-only "UI 수정"     # 프론트엔드만

# 4. 확인
kubectl get pods -n <app-namespace>
kubectl logs -f deployment/backend -n <app-namespace>

# 5. (선택) Colima 중지 (리소스 절약)
colima stop

시크릿/설정만 변경할 때

# secrets.yaml 수정 후
kubectl apply -f k8s/secrets.yaml

# 시크릿 변경은 Pod를 자동 재시작하지 않음 → 수동 재시작 필요
kubectl rollout restart deployment/backend -n <app-namespace>

롤백

# 이전 버전으로 롤백
kubectl rollout undo deployment/backend -n <app-namespace>
kubectl rollout undo deployment/frontend -n <app-namespace>

# 특정 리비전으로 롤백
kubectl rollout history deployment/backend -n <app-namespace>
kubectl rollout undo deployment/backend -n <app-namespace> --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      @      <NLB Public IP>       300
A      www    <NLB Public IP>       300

NLB Public IP 확인:

kubectl get svc -n ingress-nginx ingress-nginx-controller \
  -o jsonpath='{.status.loadBalancer.ingress[0].ip}'

SSL 인증서

cert-manager + ClusterIssuer가 설정되어 있으면 Ingress에 annotation만 추가하면 자동 발급/갱신됨.

# Ingress에 추가
metadata:
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  tls:
    - hosts:
        - www.example.com
      secretName: <app>-tls   # cert-manager가 자동 생성

인증서 상태 확인:

kubectl get certificate -n <app-namespace>
kubectl describe certificate <app>-tls -n <app-namespace>

10. 트러블슈팅

이미지 Pull 실패 (ImagePullBackOff)

kubectl describe pod <pod-name> -n <app-namespace>
# Events 섹션에서 에러 확인

# 원인 1: OCIR 인증 실패 → pull secret 재생성
kubectl delete secret ocir-secret -n <app-namespace>
kubectl create secret docker-registry ocir-secret \
  --namespace <app-namespace> \
  --docker-server=<region>.ocir.io \
  --docker-username='<namespace>/oracleidentitycloudservice/<email>' \
  --docker-password='<auth-token>'

# 원인 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)

# Pod 로그 확인
kubectl logs deployment/backend -n <app-namespace> | grep -i "hikari\|oracle\|connection"

# 원인 1: Wallet이 마운트되지 않음
kubectl exec deployment/backend -n <app-namespace> -- ls /etc/oracle/wallet/

# 원인 2: ORACLE_DSN에 TNS_ADMIN 경로가 잘못됨
# 형식: <tns-alias>_medium?TNS_ADMIN=/etc/oracle/wallet

Let's Encrypt 인증서 발급 실패

# Challenge 상태 확인
kubectl get challenges -n <app-namespace>
kubectl describe challenge <name> -n <app-namespace>

# 원인: VCN Security List에서 80번 포트가 막혀있음
# OCI Console → VCN → Security Lists → Ingress Rules에 HTTP 80 허용 추가

kubectl 인증 실패

# kubeconfig 재생성
oci ce cluster create-kubeconfig \
  --cluster-id <cluster-ocid> \
  --file $HOME/.kube/config \
  --region <region> \
  --token-version 2.0.0 \
  --kube-endpoint PUBLIC_ENDPOINT

# OCI CLI 프로파일 지정 (여러 프로파일 사용 시)
# kubeconfig의 args에 --profile <PROFILE_NAME> 추가

Colima 시작 후 kubectl 안 됨

# Colima가 kubectl 컨텍스트를 가져감
kubectl config get-contexts
kubectl config use-context <oke-context-name>  # OKE로 복원

11. 유용한 kubectl 명령어

# ── 상태 확인 ──
kubectl get pods -n <ns>                          # Pod 목록
kubectl get pods -n <ns> -w                       # 실시간 감시
kubectl get svc -n <ns>                           # Service 목록
kubectl get ingress -n <ns>                       # Ingress 확인
kubectl top pods -n <ns>                          # 리소스 사용량

# ── 로그 ──
kubectl logs deployment/backend -n <ns>           # 최근 로그
kubectl logs deployment/backend -n <ns> -f        # 실시간 로그
kubectl logs deployment/backend -n <ns> --previous # 이전 Pod 로그 (크래시 시)

# ── 디버깅 ──
kubectl describe pod <pod> -n <ns>                # Pod 상세 (이벤트 포함)
kubectl exec -it deployment/backend -n <ns> -- sh # Pod 접속
kubectl port-forward svc/backend 8000:8000 -n <ns> # 로컬 포트 포워딩

# ── 배포 관리 ──
kubectl rollout status deployment/backend -n <ns>  # 롤아웃 상태
kubectl rollout restart deployment/backend -n <ns> # 재시작 (설정 변경 반영)
kubectl rollout undo deployment/backend -n <ns>    # 이전 버전 롤백
kubectl rollout history deployment/backend -n <ns> # 배포 이력

# ── 설정 변경 ──
kubectl apply -f k8s/configmap.yaml               # ConfigMap 업데이트
kubectl apply -f k8s/secrets.yaml                 # Secret 업데이트
kubectl edit deployment backend -n <ns>            # 직접 수정 (비추천)

# ── 정리 ──
kubectl delete pod <pod> -n <ns>                  # Pod 강제 재시작
kubectl scale deployment/backend --replicas=0 -n <ns>  # 일시 중지
kubectl scale deployment/backend --replicas=1 -n <ns>  # 복원