diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b0a99af --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +target/ +*.log +.idea/ +.vscode/ +.git/ +.gitignore +.dockerignore +Dockerfile +docker-compose.yml +k8s/ +README*.md +*.iml +HELP.md diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8a0782b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,35 @@ +# syntax=docker/dockerfile:1.7 +# Multi-stage build for egovframe-boot-sample-java-config. +# Stage 1 builds the executable Spring Boot jar with Maven. +# Stage 2 runs it on a minimal Temurin JRE 17 image as a non-root user. + +# ---------- Build ---------- +FROM maven:3.9-eclipse-temurin-17 AS build +WORKDIR /workspace + +COPY pom.xml ./ +RUN --mount=type=cache,target=/root/.m2 \ + mvn -B -ntp dependency:go-offline + +COPY src ./src +RUN --mount=type=cache,target=/root/.m2 \ + mvn -B -ntp -DskipTests package && \ + cp target/*.jar /workspace/app.jar + +# ---------- Runtime ---------- +FROM eclipse-temurin:17-jre-alpine AS runtime + +RUN addgroup -S app && adduser -S -G app app +USER app:app + +WORKDIR /app +COPY --from=build --chown=app:app /workspace/app.jar /app/app.jar + +ENV JAVA_OPTS="-XX:MaxRAMPercentage=75 -XX:+ExitOnOutOfMemoryError" + +EXPOSE 8080 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \ + CMD wget -qO- http://127.0.0.1:8080/actuator/health || exit 1 + +ENTRYPOINT ["sh","-c","exec java $JAVA_OPTS -jar /app/app.jar"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4b42467 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,8 @@ +services: + app: + build: . + image: egovframe-boot-sample-java-config:${APP_VERSION:-5.0.0} + container_name: egov-boot-sample + ports: + - "8080:8080" + restart: unless-stopped diff --git a/k8s/README.md b/k8s/README.md new file mode 100644 index 0000000..51e093c --- /dev/null +++ b/k8s/README.md @@ -0,0 +1,136 @@ +# egovframe-boot-sample-java-config — 컨테이너/쿠버네티스 운영 안내 + +## 사전 요구 사항 + +| 도구 | 최소 버전 | 용도 | +|------|-----------|------| +| Docker | 24 이상 | 이미지 빌드 및 Compose 실행 | +| kubectl | 1.27 이상 | 쿠버네티스 배포 | +| (선택) minikube / kind | — | 로컬 클러스터 | + +--- + +## 1. 이미지 빌드 + +```bash +# 프로젝트 루트에서 실행 +docker build -t egovframe-boot-sample-java-config:5.0.0 . +``` + +멀티 스테이지 빌드로 Maven 컴파일 → 실행 이미지(JRE 17 Alpine)를 순서대로 생성합니다. +빌드 캐시(`/root/.m2`)를 활용하려면 BuildKit을 활성화(`DOCKER_BUILDKIT=1`)하세요. + +--- + +## 2. Docker Compose로 로컬 실행 + +```bash +# 실행 +docker compose up -d + +# 로그 확인 +docker compose logs -f + +# 중지 및 볼륨 삭제 +docker compose down -v +``` + +접속: http://localhost:8080 + +헬스 확인: +```bash +curl -s http://localhost:8080/actuator/health +# {"status":"UP"} +``` + +--- + +## 3. 쿠버네티스 배포 + +### 3-1. 이미지 레지스트리 설정 + +`k8s/deployment.yaml`의 `image` 값을 실제 레지스트리 경로로 변경합니다. + +```yaml +# 예시 +image: ghcr.io//egovframe-boot-sample-java-config:5.0.0 +``` + +로컬 클러스터(minikube)를 사용하는 경우 이미지를 직접 로드합니다. + +```bash +minikube image load egovframe-boot-sample-java-config:5.0.0 +``` + +### 3-2. 매니페스트 적용 + +```bash +kubectl apply -f k8s/service.yaml +kubectl apply -f k8s/deployment.yaml +``` + +### 3-3. 배포 상태 확인 + +```bash +# Pod 상태 +kubectl get pods -l app.kubernetes.io/name=egovframe-boot-sample-java-config + +# Deployment 롤아웃 완료 대기 +kubectl rollout status deployment/egovframe-boot-sample-java-config +``` + +--- + +## 4. 접속 + +### ClusterIP (service.yaml 기준) + +service.yaml은 `type: ClusterIP`로 설정되어 있으며, 클러스터 내부에서 `egovframe-boot-sample-java-config:8080`으로 접근할 수 있습니다. + +### 포트 포워딩 (개발/테스트 용) + +```bash +kubectl port-forward service/egovframe-boot-sample-java-config 8080:8080 +``` + +접속: http://localhost:8080 + +--- + +## 5. 헬스체크 확인 + +애플리케이션이 `/actuator/health` 엔드포인트를 제공합니다. + +```bash +# 전체 상태 +curl -s http://localhost:8080/actuator/health + +# readiness +curl -s http://localhost:8080/actuator/health/readiness + +# liveness +curl -s http://localhost:8080/actuator/health/liveness +``` + +정상 응답 예시: +```json +{"status":"UP"} +``` + +k8s Probe 설정 요약: + +| Probe | 경로 | initialDelaySeconds | periodSeconds | +|-------|------|---------------------|---------------| +| readiness | /actuator/health/readiness | 20 | 10 | +| liveness | /actuator/health/liveness | 60 | 20 | + +Pod가 `Running` 상태이고 `READY` 열이 `1/1`이면 정상입니다. + +--- + +## 6. 삭제 + +```bash +kubectl delete -f k8s/deployment.yaml +kubectl delete -f k8s/service.yaml +``` diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml new file mode 100644 index 0000000..72c7665 --- /dev/null +++ b/k8s/deployment.yaml @@ -0,0 +1,69 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: egovframe-boot-sample-java-config + labels: + app.kubernetes.io/name: egovframe-boot-sample-java-config + app.kubernetes.io/part-of: egovframe-sample +spec: + replicas: 1 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + selector: + matchLabels: + app.kubernetes.io/name: egovframe-boot-sample-java-config + template: + metadata: + labels: + app.kubernetes.io/name: egovframe-boot-sample-java-config + app.kubernetes.io/part-of: egovframe-sample + spec: + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + containers: + - name: app + # Replace with your registry/tag, e.g. ghcr.io//egovframe-boot-sample-java-config:5.0.0 + image: egovframe-boot-sample-java-config:5.0.0 + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 8080 + resources: + requests: + cpu: "200m" + memory: "384Mi" + limits: + cpu: "800m" + memory: "768Mi" + readinessProbe: + httpGet: + path: /actuator/health/readiness + port: http + initialDelaySeconds: 20 + periodSeconds: 10 + failureThreshold: 6 + livenessProbe: + httpGet: + path: /actuator/health/liveness + port: http + initialDelaySeconds: 60 + periodSeconds: 20 + failureThreshold: 3 + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + volumeMounts: + - name: tmp + mountPath: /tmp + volumes: + - name: tmp + emptyDir: {} diff --git a/k8s/service.yaml b/k8s/service.yaml new file mode 100644 index 0000000..68d8edc --- /dev/null +++ b/k8s/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: egovframe-boot-sample-java-config + labels: + app.kubernetes.io/name: egovframe-boot-sample-java-config + app.kubernetes.io/part-of: egovframe-sample +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: egovframe-boot-sample-java-config + ports: + - name: http + port: 8080 + targetPort: http diff --git a/pom.xml b/pom.xml index 217723c..633f362 100644 --- a/pom.xml +++ b/pom.xml @@ -110,6 +110,10 @@ org.projectlombok lombok + + org.springframework.boot + spring-boot-starter-actuator + org.springframework.boot spring-boot-starter-test diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 3b59fc5..24e49c4 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,5 +1,10 @@ server.port=8080 +# Actuator 헬스 엔드포인트 설정 (Docker HEALTHCHECK / k8s probe 대응) +management.endpoints.web.exposure.include=health +management.endpoint.health.show-details=never +management.endpoint.health.probes.enabled=true + # \uc218\ub3d9 Bean\uc774 \uc790\ub3d9 Bean\uc744 \uc624\ubc84\ub77c\uc774\ub529\ud558\uac8c \uc124\uc815 spring.main.allow-bean-definition-overriding=true