From e10e3a8d5eeb080f367c493ca0db27242a0606b1 Mon Sep 17 00:00:00 2001 From: dasomel Date: Wed, 20 May 2026 18:27:12 +0900 Subject: [PATCH 1/7] feat(k8s): harden Deployment and align Service selector The existing k8s/deployment.yaml had only the bare minimum (selector, image, containerPort), and k8s/service.yaml advertised a selector ('egovframe-web-sample') that did not match the Deployment's labels ('egovframe-web'), so the Service routed to no pods. Deployment changes: - explicit replicas=1 and RollingUpdate strategy - pod-level securityContext (runAsNonRoot, runAsUser/Group/fsGroup=1000) - container securityContext (allowPrivilegeEscalation=false, drop ALL) - resources.requests {cpu: 250m, memory: 512Mi} and resources.limits {cpu: 1000m, memory: 1Gi} - readinessProbe and livenessProbe on / via the named 'http' port - ephemeral /usr/local/tomcat/{work,temp,logs} volumes so the pod tolerates a readOnlyRootFilesystem profile if cluster policy adds it - consistent app.kubernetes.io/* recommended labels Service changes: - selector aligned to 'app: egovframe-web' (matches the Deployment) - name normalised to 'egovframe-web' (was 'egovframe-web-sample') - type changed from NodePort (hard-coded nodePort 30000) to ClusterIP; cluster operators can layer an Ingress or NodePort overlay on top - targetPort references the named 'http' port instead of a bare 8080 - recommended labels added --- k8s/deployment.yaml | 60 ++++++++++++++++++++++++++++++++++++++++++++- k8s/service.yaml | 17 +++++++------ 2 files changed, 69 insertions(+), 8 deletions(-) diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml index 659de23..51a48ab 100644 --- a/k8s/deployment.yaml +++ b/k8s/deployment.yaml @@ -4,7 +4,15 @@ metadata: name: egovframe-web labels: app: egovframe-web + app.kubernetes.io/name: egovframe-web + app.kubernetes.io/part-of: egovframe-sample spec: + replicas: 1 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 selector: matchLabels: app: egovframe-web @@ -12,9 +20,59 @@ spec: metadata: labels: app: egovframe-web + app.kubernetes.io/name: egovframe-web + app.kubernetes.io/part-of: egovframe-sample spec: + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 containers: - name: egovframe-web + # Replace with your registry/tag, e.g. ghcr.io//egovframe-web:5.0.0 image: egovframe-web:5.0.0 + imagePullPolicy: IfNotPresent ports: - - containerPort: 8080 + - name: http + containerPort: 8080 + resources: + requests: + cpu: "250m" + memory: "512Mi" + limits: + cpu: "1000m" + memory: "1Gi" + readinessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 30 + periodSeconds: 10 + failureThreshold: 6 + livenessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 90 + periodSeconds: 20 + failureThreshold: 3 + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + volumeMounts: + - name: tomcat-work + mountPath: /usr/local/tomcat/work + - name: tomcat-temp + mountPath: /usr/local/tomcat/temp + - name: tomcat-logs + mountPath: /usr/local/tomcat/logs + volumes: + - name: tomcat-work + emptyDir: {} + - name: tomcat-temp + emptyDir: {} + - name: tomcat-logs + emptyDir: {} diff --git a/k8s/service.yaml b/k8s/service.yaml index c9b4f11..9714168 100644 --- a/k8s/service.yaml +++ b/k8s/service.yaml @@ -1,13 +1,16 @@ apiVersion: v1 kind: Service metadata: - name: egovframe-web-sample + name: egovframe-web + labels: + app: egovframe-web + app.kubernetes.io/name: egovframe-web + app.kubernetes.io/part-of: egovframe-sample spec: + type: ClusterIP selector: - app: egovframe-web-sample + app: egovframe-web ports: - - port: 8080 - targetPort: 8080 - nodePort: 30000 - type: NodePort - \ No newline at end of file + - name: http + port: 8080 + targetPort: http From 0474766f5346b59d04d64ebf8d124bd7e5b76b56 Mon Sep 17 00:00:00 2001 From: dasomel Date: Wed, 27 May 2026 23:32:27 +0900 Subject: [PATCH 2/7] =?UTF-8?q?fix(k8s):=20readinessProbe/livenessProbe=20?= =?UTF-8?q?path=EB=A5=BC=20/app/=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dockerfile이 WAR를 webapps/app.war로 복사하므로 Tomcat 컨텍스트 경로가 /app이다. 기존 path: / 는 404 응답할 가능성이 있어 컨테이너가 unhealthy 상태로 재시작될 수 있다. 실제 컨텍스트 경로에 맞춰 /app/으로 수정한다. 리뷰 의견 반영 (eGovFrameSupport). --- k8s/deployment.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml index 51a48ab..de2a109 100644 --- a/k8s/deployment.yaml +++ b/k8s/deployment.yaml @@ -45,14 +45,14 @@ spec: memory: "1Gi" readinessProbe: httpGet: - path: / + path: /app/ port: http initialDelaySeconds: 30 periodSeconds: 10 failureThreshold: 6 livenessProbe: httpGet: - path: / + path: /app/ port: http initialDelaySeconds: 90 periodSeconds: 20 From b9e8dfd872836d1608e0d1345430229c2c7bfd3f Mon Sep 17 00:00:00 2001 From: dasomel Date: Wed, 27 May 2026 23:33:52 +0900 Subject: [PATCH 3/7] =?UTF-8?q?fix(k8s):=20Pod-level=20securityContext?= =?UTF-8?q?=EC=97=90=EC=84=9C=20UID=201000=20=EA=B0=95=EC=A0=9C=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tomcat 공식 이미지는 /usr/local/tomcat 하위 디렉터리가 root 소유 기반으로 설계되어 있어 UID 1000으로 강제 실행하면 webapps/work/logs 디렉터리 권한 부족으로 시작 실패 가능성이 있다. Pod-level runAsNonRoot/runAsUser/runAsGroup/ fsGroup 설정을 제거하고 컨테이너의 기본 사용자(root)로 실행되도록 둔다. 컨테이너 레벨 보안 설정(allowPrivilegeEscalation: false, capabilities.drop: ALL) 은 유지되어 권한 상승 및 추가 capability는 차단된다. 리뷰 의견 반영 (eGovFrameSupport). --- k8s/deployment.yaml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml index de2a109..9ab7ffb 100644 --- a/k8s/deployment.yaml +++ b/k8s/deployment.yaml @@ -23,11 +23,6 @@ spec: app.kubernetes.io/name: egovframe-web app.kubernetes.io/part-of: egovframe-sample spec: - securityContext: - runAsNonRoot: true - runAsUser: 1000 - runAsGroup: 1000 - fsGroup: 1000 containers: - name: egovframe-web # Replace with your registry/tag, e.g. ghcr.io//egovframe-web:5.0.0 From 2d1ed4224bcecb91fa35ea153afa2a09a1c957bf Mon Sep 17 00:00:00 2001 From: dasomel Date: Wed, 27 May 2026 23:35:44 +0900 Subject: [PATCH 4/7] =?UTF-8?q?fix(k8s):=20readOnlyRootFilesystem=20?= =?UTF-8?q?=EC=97=86=EC=9D=B4=20=ED=9A=A8=EA=B3=BC=20=EC=97=86=EB=8A=94=20?= =?UTF-8?q?emptyDir/volumeMount=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit readOnlyRootFilesystem 설정 없이 work/temp/logs를 emptyDir로 마운트해도 Tomcat이 자체적으로 사용하는 디렉터리에 별도 영향 없이 정상 동작하므로 설정의 의미가 없어 제거한다. 컨테이너 레벨 보안(allowPrivilegeEscalation: false, capabilities.drop: ALL)은 유지되어 권한 상승 및 추가 capability 획득은 차단된다. readOnlyRootFilesystem 기반의 더 강한 격리는 Tomcat의 webapps unpackWARs 동작 호환성을 확보한 별도 PR로 다루는 것이 적절하다고 판단했다. 리뷰 의견 반영 (eGovFrameSupport). --- k8s/deployment.yaml | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml index 9ab7ffb..daeb875 100644 --- a/k8s/deployment.yaml +++ b/k8s/deployment.yaml @@ -57,17 +57,3 @@ spec: capabilities: drop: - ALL - volumeMounts: - - name: tomcat-work - mountPath: /usr/local/tomcat/work - - name: tomcat-temp - mountPath: /usr/local/tomcat/temp - - name: tomcat-logs - mountPath: /usr/local/tomcat/logs - volumes: - - name: tomcat-work - emptyDir: {} - - name: tomcat-temp - emptyDir: {} - - name: tomcat-logs - emptyDir: {} From 77a82bf4f6b02093db51ce9fce2179cae3d4cc00 Mon Sep 17 00:00:00 2001 From: dasomel Date: Wed, 27 May 2026 23:37:22 +0900 Subject: [PATCH 5/7] =?UTF-8?q?fix(k8s):=20Service=20=ED=83=80=EC=9E=85?= =?UTF-8?q?=EC=9D=84=20NodePort(30000)=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=ED=95=98=EC=97=AC=20README=EC=99=80=20=EC=9D=BC=EA=B4=80?= =?UTF-8?q?=EC=84=B1=20=ED=99=95=EB=B3=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit k8s/README.md가 minikube 로컬 시연용 NodePort(30000) 접속을 전제로 작성되어 있는데 service.yaml은 ClusterIP로 설정되어 두 설정이 충돌했다. README의 접속 안내(http://:30000/app/)와 맞도록 Service 타입을 NodePort로 변경하고 nodePort를 30000으로 명시한다. 리뷰 의견 반영 (eGovFrameSupport). --- k8s/service.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/k8s/service.yaml b/k8s/service.yaml index 9714168..f0485f7 100644 --- a/k8s/service.yaml +++ b/k8s/service.yaml @@ -7,10 +7,11 @@ metadata: app.kubernetes.io/name: egovframe-web app.kubernetes.io/part-of: egovframe-sample spec: - type: ClusterIP + type: NodePort selector: app: egovframe-web ports: - name: http port: 8080 targetPort: http + nodePort: 30000 From 8d55c2a321136ae5ef272c09f046f969a94367c9 Mon Sep 17 00:00:00 2001 From: dasomel Date: Mon, 1 Jun 2026 23:51:57 +0900 Subject: [PATCH 6/7] =?UTF-8?q?fix:=20Dockerfile=20=EB=B2=A0=EC=9D=B4?= =?UTF-8?q?=EC=8A=A4=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EB=B0=8F=20WAR=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tomcat:8.5-jre8(단종) → tomcat:10.1-jdk17-temurin으로 교체하고, COPY 대상 파일명을 pom.xml finalName 기준 실제 빌드 산출물인 egovframe-web-5.0.0.war로 정정한다. 기존 web-example-1.0.0.war 경로는 존재하지 않아 이미지 빌드가 실패하던 문제를 해결한다. --- Dockerfile | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4c9d11a..cf42a04 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,12 @@ ######################################################### -# 목적 : web-sample-1.0.0.war를 tomcat으로 구동하는 도커 이미지 생성 +# 목적 : egovframe-web-5.0.0.war를 tomcat으로 구동하는 도커 이미지 생성 # 실행방법 : 프로젝트 루트디렉토리에서(Dockerfile 경로) 아래 명령어 실행 -# (명령어) docker build . -t egovframe-web-sample:1.0.0 +# (명령어) docker build . -t egovframe-web-sample:5.0.0 ######################################################### -# tomcat base image설정 (8.5-jre8) -FROM tomcat:8.5-jre8 -COPY ./target/web-example-1.0.0.war /usr/local/tomcat/webapps/app.war +# tomcat base image 설정 (10.1-jdk17-temurin) +FROM tomcat:10.1-jdk17-temurin +COPY ./target/egovframe-web-5.0.0.war /usr/local/tomcat/webapps/app.war # tomcat의 web서버 포트설정 EXPOSE 8080 # 컨테이너 시작 시 tomcat이 구동되도록 해당 명령을 실행 From cb799f5339fd200bdacbda3b6d3b9b2cfab1a236 Mon Sep 17 00:00:00 2001 From: dasomel Date: Fri, 5 Jun 2026 01:41:57 +0900 Subject: [PATCH 7/7] =?UTF-8?q?docs:=20k8s=20README=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=EB=AA=85=20=ED=86=B5=EC=9D=BC=20=EB=B0=8F=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=EB=B3=84=20=EC=A0=91=EC=86=8D=20=EC=95=88=EB=82=B4=20?= =?UTF-8?q?=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dockerfile 주석의 이미지 태그가 egovframe-web-sample:5.0.0으로 deployment.yaml 및 README와 불일치하는 문제를 수정. 이미지명을 egovframe-web:5.0.0 하나로 통일. k8s/README.md에 docker build 명령을 명시하고, minikube와 Docker Desktop/kind 환경별 이미지 적재 필요 여부 및 접속 URL을 표로 정리. --- Dockerfile | 2 +- k8s/README.md | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index cf42a04..ec05a27 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ ######################################################### # 목적 : egovframe-web-5.0.0.war를 tomcat으로 구동하는 도커 이미지 생성 # 실행방법 : 프로젝트 루트디렉토리에서(Dockerfile 경로) 아래 명령어 실행 -# (명령어) docker build . -t egovframe-web-sample:5.0.0 +# (명령어) docker build . -t egovframe-web:5.0.0 ######################################################### # tomcat base image 설정 (10.1-jdk17-temurin) diff --git a/k8s/README.md b/k8s/README.md index 3f47905..c8f0882 100644 --- a/k8s/README.md +++ b/k8s/README.md @@ -1,9 +1,22 @@ ## K-Pass(Kubernetes) 배포 설정 - web-sample 프로젝트를 kubernetes 환경에 배포하기 위한 설정파일 -- 로컬PC에 minikube가 구성된 환경을 가정 +- 로컬PC에 minikube 또는 Docker Desktop / kind가 구성된 환경을 가정 --- -### minikube 내 도커 이미지 로드 +### 도커 이미지 빌드 +- 프로젝트 루트 디렉토리(Dockerfile 위치)에서 아래 명령어로 이미지를 생성 + - ```docker build -t egovframe-web:5.0.0 .``` +- 빌드된 이미지 확인 + - ```docker image ls | grep egovframe-web``` + +### 실행 환경별 이미지 적재 및 접속 방법 + +| 환경 | 이미지 적재 | 웹 접속 URL | +|---|---|---| +| **minikube** | `minikube image load egovframe-web:5.0.0` 필요 | `http://:30000/app/` | +| **Docker Desktop / kind** | 별도 적재 불필요 (로컬 Docker 이미지 그대로 사용) | `http://localhost:30000/app/` | + +### minikube 내 도커 이미지 로드 (minikube 사용 시) - 대상 이미지 --> egovframe-web:5.0.0 - 대상 이미지 확인 명령어 ``` docker image ls ``` - minikube 내 이미지 로드