로컬의 경우 ARM, x86 68bit 두 가지 환경이 있다.
서버 환경도 보통 후자인데 크로스 플랫폼으로 빌드하지 않는 이상, 최신 버전의 맥북은 빌드시 ARM 으로 된다.
서비스 배포시 우리는 어떤 환경에서던 동일한 Dockerfile을 작성해서 컨테이너 이미지를 빌드한다.
어떻게 동일한 컨테이너 이미지를 이용해서 Docker와 kubernetes에서 이용 가능할까? 이에 대해 알려면 Docker에 대한 살짝 더 깊은 이해가 요구된다.
OCI(Open Container Initiative) : 저수준 컨테이너 런타임
OCI 이미지 스펙을 가진 컨테이너 이미지는 OCI 런타임에서 구동 가능

- 실제 실행할려면 isolation된 환경이 필요한데, 관련된 리눅스 기술을 관리하는 레이어
- Linux 운영체제의 커널에 더 가까운 런타임
컨테이너는 namespace와 cgroup을 사용하여 범위 내의 명령을 실행
- namespace: 각 컨테이너에 대해 파일 시스템이나 네트워킹과 같은 시스템 리소스를 가상화
- cgroup: 각 컨테이너가 사용할 수 있는 CPU 및 메모리와 같은 리소스 양을 제한
CRI(Container Runtime Interface)
CRI 런타임 스펙을 지원하는 컨테이너 런타임은 K8S에서 사용 가능

doc : https://kubernetes.io/ko/docs/concepts/architecture/cri/
- k8s에서 만든 런타임 인터페이스로, 명확한 추상계층을 정의해서 컨테이너 런타임 구축에만 집중할 수 있게 한다.
- 컨테이너의 생명 주기와 이미지 등을 관리
- k8s에서 다양한 컨테이너 런타임을 사용할 수 있도록 하는 플러그인
Docker는 docker-containerd라는 고수준 런타임과 docker-runc라는 저수준 런타임을 이용해서 제공한다.
그러나 Docker(dockershim) 지원은 비효율적이라, 현재 kubernetes는 v1.24부터 docker를 지원하지 않는다.
ㅁ뭐? 도커가 지원을 안해?
쿠버네티스에서 지원하는 CRI에서 OCI 이미지 스펙을 가지는 이미지(Docker, Kniko, Jib, Buildah…)는 동작 가능하다는 점이다.
아무튼, JAR 파일을 컨테이너 내에서 생성해서 배포시 종속 문제 발생을 최소화하고자 한다.
- 로컬에서 JAR 패키징 및 동작 확인
- 컨테이너 이미지 생성(빌드 단계 포함)
- 기존 방식 : JAR파일을 로컬에서 빌드 → COPY
- CRI 방식 : Docker Image 안에서 JAR을 만들어 쓴다 (JAR파일 외부 빌드의 종속성 문제 해결)
- 멀티 모듈 지원하도록 Dockerfile 작성
FROM gradle:latest as builder
WORKDIR /workspace/app
COPY ./workspace/app/
RUN ./gradlew build -p ${MODULE}
EXPOSE 8080
ENDPOINT ["java", "-jar", \
"${MODULE}/build/libs/${module}.jar"]
기존 build.gradle 파일 수정
bootJar {
archiveFileName = "${project.name}.jar"
}
나는 AWS Cloud9에서 작업했기에 환경변수로 MODULE 값을 관리했음
export MODULE=membership-service VERSION=1.0.1
컨테이너 이미지 최적화의 이해
컨테이너 이미지 최적화의 목표 : 불필요한 파일이나 패키지를 제거하여 경량의 이미지 생성
- 성능 향상 : 불필요한 Layer나 파일이 있는 경우, 시스템 리소스 소비를 증가
- 자원 효율성 : 불필요한 데이터 저장을 위한 저장소 공간 차지
- 보안 : 불필요한 컴포넌트, 패키지 등이 보안 취약점을 노출
- 배포시간 : 배포/롤백 시간을 단축
- CI 효율성 : 시간과 자원 절약
- 스케일링 및 관리
1. 리눅스의 오버레이 파일 시스템에 대한 이해가 필요하다.
https://tech.kakaoenterprise.com/171
실제로 컨테이너에서 사용할 이미지는 중복을 해결하기 위해 여러 "레이어"들의 조합으로 제공된다. 이렇게 조합된 레이어들을 하나의 뷰로 제공하는게 오버레이 파일 시스템인겨
쓰기가능한 Layer와 읽기 전용의 Layer를 합친 새로운 Layer 영역에서 모든 층의 파일들이 보인다.
이때, Merged Layer에서 생성한 파일은 쓰기가능한 Layer에 저장된다.
사실 아직 잘 몰라서 공부가 필요하다. 위의 문장 정도로만 이해하는 수준
2. 멀티 스테이지 빌드도 잠깐 알고 가자.
이미지 빌드시 필요하지만, 최종 컨테이너 이미지에는 필요 없는 환경을 제거할 수 있도록 단계를 나누어, 기반 이미지를 만드는 방법

Dockerfile 내에서 여러 Stage를 사용해서 컨테이너 이미지를 구축
-
- 이미지 크기 감소, 보안 강화, 빠른 빌드 및 배포
3. 베이직 이미지 : 컨테이너 이미지 생성시 사용되는 기본 이미지
→ 컨테이너의 OS와 런타임 환경을 제공
gradle:7.6.4-jdk11 (압축: 345.32MB)
openjdk:11.0 (압축 : 314.4MB)
openjdk:11-slim-stretch (압축: 210.39MB)
openjdk:11-jre-slim (압축: 75.33MB) 엄청 작아진다
./gradlew build로 JAR 뽑아보면 기존 이미지의 용량을 잘 확인해보자.
4. 최적화 기법
- dockerhub에서 openjdk 검색 → 버전에 맞는 녀석을 찾는다
- Ubuntu 계열의 debian 버전에 따라서 buster나 bullseye 중 후자가 더 최신버전
- slim을 써야 공간 크기가 작다
FROM gradle:jkd11-alpine as builder
WORDIR /workspace/app
COPY . /workspace/app/
RUN ./gradlew build -p ${MODULE}
FROM openjdk:11-jre-slim
RUN groupadd -r appuser && useradd -r -g appuser appuser
COPY --from=builder /workspace/app/${MODULE}/build/libs/${MODULE}.jar ./${MODULE}.jar
EXPOSE 8080
USER appuser
ENTRYPOINT ["java","-jar","${MODULE}.jar"]
Docker는 기본적으로 root 유저로 작업하기 때문에, 버전이 오래된 코드로 작업할때 각종 취약점이 발생할 수 있다. 그래서 root 계정으로는 실행되면 안되도록 설계해야 한다.
- 새로운 유저를 만들어서 group에 넣고 리눅스 시스템 유저 권한으로 생성
RUN groupadd -r appuser && useradd -r -g appuser appuser
결과 : 진행중인 프로젝트에서 멀티 스테이지 빌드와 베이스 이미지 교체를 통해서 기존의 700MB 에서 200MB로 개선할 수 있었다.
No candidates found for method call plugins 오류 : Gradle을 Reload해서 해결했다.
'backend' 카테고리의 다른 글
HTTP multipart/form-data 파일 업로드 문제 해결 (0) | 2024.11.21 |
---|---|
Hexagonal 아키텍처와 MVC 패턴 비교 (0) | 2024.11.21 |
Spring Cloud를 뜯어보자 (Gateway, Config, Netflix Eureka) (0) | 2024.11.19 |
Github Action으로 시작하는 GitOps 파이프라인(AWS ECR, Github Actions) (0) | 2024.11.19 |
Docker 데몬 없이 컨테이너 이미지 빌드(kubernetes Docker 지원 중단) (0) | 2024.11.19 |