- 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>
23 KiB
23 KiB
OKE(Oracle Kubernetes Engine) 배포 가이드
Spring Boot + Next.js 앱을 OKE에 배포하는 전체 과정을 정리한 문서. Colima(로컬 ARM64 Docker) → OCIR(이미지 레지스트리) → OKE(K8s 클러스터) 파이프라인 기준.
목차
- 사전 준비
- 인프라 아키텍처
- 최초 클러스터 설정 (1회성)
- K8s 매니페스트 구조
- Dockerfile 작성
- 배포 스크립트 (deploy.sh)
- 일상적인 배포 절차
- 환경별 설정 관리
- 도메인 및 SSL 설정
- 트러블슈팅
- 유용한 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.ts에output: "standalone"필수NEXT_PUBLIC_*환경변수는 빌드 시점에만 주입 가능 (런타임 X).next/static과public/은 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> # 복원