[Docker] 3. Application build
❗️이 글은 엘튼 스톤맨의 "도커 교과서" 교재를 참고하여 작성하였습니다.
목차
1. 빌드 서버가 필요할까?
2. 애플리케이션 빌드 예제: JAVA
3. 애플리케이션 빌드 예제: Node.js
4. 애플리케이션 빌드 예제: Go
5. Multi-Stage Dockerfile 스크립트
6. 연습 문제
1. 빌드 서버가 필요할까?
소프트웨어 프로젝트를 빌드하려면 개발 팀과 빌드 서버는 모든 도구를 같은 버전으로 사용해야 한다.
빌드 서버와 버전이 달라지는 것만으로도 빌드가 실패할 수 있기 때문이다.
도커를 사용하여 빌드 툴체인 한 번에 패키징,
- 개발에 필요한 도구 배포하는 Dockerfile 스크립트 작성 후 이미지로 만들기
- 이미지를 사용해 소스코드 컴파일함으로써 애플리케이션 패키징
📍멀티 스테이지 빌드 적용
FROM diamol/base AS build-stage
RUN echo 'Building...' > /build.txt
FROM diamol/base AS test-stage
COPY --from=build-stage /build.txt /build.txt
RUN echo 'Testing...' >> /build.txt
FROM diamol/base
COPY --from=test-stage /build.txt /build.txt
CMD cat /build.txt
- 위의 스크립트는 빌드가 여러 단계로 나뉘는 멀티 스테이지 빌드를 적용한 것이다.
- 각 빌드 단계는 FROM으로 시작하며 AS 파라미터를 이용해 이름을 붙일 수 있다.
- 여러 단계로 나뉘어 있지만, 최종 산출물은 마지막 단계의 내용물을 담은 도커 이미지다.
- 각 빌드 단계는 독립적으로 실행되지만, 앞 단계에서 만들어진 디렉터리나 파일을 복사할 수 있다.
이 예제에서는,
- build-stage 단계에서 build.txt 파일을 생성
- (1)에서 생성된 파일을 test-stage로 복사하고 파일 생성
- (2)에서 생성된 파일을 복사
- Run 인스트럭션
: 빌드 중에 컨테이너 안에서 명령을 실행한 다음 그 결과를 이미지 레이어에 저장
- FROM 인스트럭션에서 지정한 이미지에서 실행할 수 있는 것이어야 한다. (diamol/base)
- diamol/base 이미지는 echo 명령을 포함하기 때문에 정상적으로 작동
📍 멀티 스테이지 빌드 적용한 dockerfile 스크립트 실행 과정
- 1단계 빌드에서 텍스트 파일 생성
- (1)에서 생성한 텍스트 파일 복사하고 파일 생성
- (2)에서 생성한 텍스트 파일 복사
✂️ 주의
- 각 빌드 단계는 서로 격리
- 빌드 단계별로 기반 이미지가 달라 사용할 수 있는 도구도 다를 수 있다.
- 한 단계에서라도 명령이 실패하면 전체 빌드가 실패한다.
📍 멀티 스테이지로 이미지 빌드하기
멀티 스테이지 Dockerfile 스크립트로 자바 애플리케이션 빌드해보자.
cd ch04/exercises/multi-stage
docker image build -t multi-stage .
Dockerfile 스크립트에 작성된 순서에 따라 빌드가 진행된다.
- [build-stage] 단계에서 build.txt 파일 생성
- [test-stage] 단계에서 build.txt 파일을 이미지에 추가
- 최종 결과물이 될 이미지 생성 => [test-stage ]단계에서 build.txt 파일 복사해온다.
📍자바 애플리케이션의 멀티 스테이지 빌드 과정
- 의존 모듈을 내려받고 애플리케이션을 빌드
- 빌드 결과물을 복사해 온 다음 테스트 수행
- 테스트가 끝난 빌드 결과물을 복사
=> 이런 방법으로 애플리케이션의 '이식성'을 확보할 수 있다.
도커만 갖춰진다면,
- 컨테이너를 통해 어떤 환경에서든 애플리케이션 빌드/실행 가능
- 새로 팀에 합류한 개발자 즉시 개발 환경 갖출 수 있다.
- 빌드 도구를 도커 이미지를 통해 중앙 집중적으로 관리 가능
2. 애플리케이션 빌드 예제: JAVA
자바 스프링부트를 사용해 구현한 것을 예제로 빌드하고 실행하는 과정을 이해해보자.
애플리케이션을 빌드하고 실행하기 위한 빌드 도구 모두 도커 이미지를 통해 가져온다.
애플리케이션은 자바 빌드 도구로 Maven과 OpenJDK를 사용한다.
- Maven: 빌드 절차와 의존 모듈의 입수 방법을 정의, 빌드 정차가 정의된 XML 문서 사용, mvn 명령 사용
- OpenJDK: 자유로이 재배포가 가능한 자바 런타임이자 개발자 키트
📍 Maven을 사용해 빌드하는 Dockerfile 스크립트
FROM diamol/maven AS builder
WORKDIR /usr/src/iotd
COPY pom.xml .
RUN mvn -B dependency:go-offline
COPY . .
RUN mvn package
# app
FROM diamol/openjdk
WORKDIR /app
COPY --from=builder /usr/src/iotd/target/iotd-service-0.1.0.jar .
EXPOSE 80
ENTRYPOINT ["java", "-jar", "/app/iotd-service-0.1.0.jar"]
FROM 인스트럭션이 여러개 => 멀티 스테이지 빌드 적용
1️⃣ builder 단계에서 하는 일
- 기반 이미지는 diamol/maven
- 이미지는 maven과 OpenJDK 포함.
- 이미지에 작업 디렉터리 만든 다음 이 디렉터리에 pom.xml 파일을 복사
- pom.xml 파일에는 maven에 수행할 빌드 절차 정의되어 있음.
- 첫 번째 RUN 인스트럭션에서 maven이 실행돼 필요한 의존 모듈 다운
- 시간이 오래 걸려 별도의 단계로 분리해 레이어 캐시 활용할 수 있도록 함.
- 새로운 의존 모듈이 추가된 경우((XML 파일 변경됐을 것)), 이 단계 다시 실행
- 새로운 의존 모듈 추가되지 않은 경우 : 이미지 캐시 재사용
- COPY 인스트럭션 통해 나머지 소스 코드 복사
- "실행중인 디렉터리에 포함된 모든 파일과 서브 디렉터리를 현재 이미지 내 작업 디렉터리로 복사"하라는 의미
- mvn package 명령 실행
- 애플리케이션을 빌드하고 패키징하라는 의미
- 입력은 자바 소스코드, 출력은 JAR 포맷으로 패키징된 자바 애플리케이션
2️⃣ builder 단계가 끝난 후
builder 단계가 끝나면, 컴파일된 애플리케이션이 해당 단계의 파일 시스템에 만들어진다.
✔️ builder 단계 실패
1) maven 빌드 과정에서 발생하는(네트워크 문제로 의존 모듈 다운 x, 소스 코드에 컴파일 에러) 오류로,
2) RUN 인스트럭션 실패하며 전체 빌드 실패
✅ builder 단계 정상적으로 완료
- 기반 이미지는 diamol/openjdk
- 자바 11 런타임 포함(메이븐은 포함 x)
- 이미지에 작업 디렉터리 만든 후 builder 단계에서 만든 JAR 파일 복사
- JAR 파일: 모든 의존 모듈과 컴파일된 애플리케이션을 포함하는 단일 파일
- 애플리케이션은 80번 포트를 주시하는 웹 서버 애플리케이션으로, EXPOSE 인스트럭션으로 외부 공개해야 함.
- ENTRYPOINT는 CMD와 같은 기능을 하는 인스트럭션
- 해당 이미지로 컨테이너가 실행되면 도커가 이 인스트럭션에 정의된 명령을 실행
- 기반 이미지는 java 명령으로 빌드된 JAR 파일 실행
📍Image build
cd ch04/exercises/image-of-the-day
docker image build -t image-of-the-day .
이 애플리케이션은 NASA의 "오늘의 천문 사진 서비스"에서 사진을 받아오는 간단한 REST API이다.
📍Docker Network 생성
docker network create nat
만약 오류가 나온다면=> nat이라는 이름의 도커 네트워크를 이미 생성했기 때문이어서 무시해도 된다.
--network 옵션
- 새로 만들 컨테이너를 연결할 네트워크 직접 지정 가능
- 같은 네트워크 안에 속한 컨테이너 간에는 자유롭게 통신 가능
📍Container run
docker container run --name iotd -d -p 800:80 --network nat image-of-the-day
http://localhost:800/image에서 NASA가 제공하는 "오늘의 사진"에 대한 정보를 JSON 포맷으로 확인 가능
✏️ 도커가 설치된 경우 => 소스 코드와 Dockerfile 스크립트만 있다면 애플리케이션 어디서든 실행할 수 있다.
• 애플리케이션 이미지에 빌드 도구는 포함되지 않는다.
• 애플리케이션 이미지에너는 도커 파일에 정의된 빌드 단계 중 마지막 단계의 콘텐츠만 포함된다.
• 이전 단계 콘텐츠를 포함시키려면 최종 단계에서 명시적으로 해당 콘텐츠를 복사해와야 한다.
3. 애플리케이션 빌드 예제: Node.js
📍 Java와 JavaScript 차이
- Java: 컴파일을 거쳐야 하기 때문에 패키징 과정을 통해 JAR 파일 생성한다.
- JavaScript: 인터프리터형 언어로 별도의 컴파일 절차가 필요없다.
- 애플리케이션 실행위해 Node.js 런타임과 소스코드가 이미지에 포함돼야 한다.
📍npm 사용해 애플리케이션 빌드
FROM diamol/node AS builder
WORKDIR /src
COPY src/package.json .
RUN npm install
# app
FROM diamol/node
EXPOSE 80
CMD ["node", "server.js"]
WORKDIR /app
COPY --from=builder /src/node_modules/ /app/node_modules/
COPY src/ .
- 멀티 스테이지 빌드를 통해 의존 모듈 로딩 최적화할 수 있다.
- npm 패키지 관리자를 사용해 의존 모듈 관리한다.
- diamol/node 이미지에 Node.js 런타임과 npm이 설치되어 있다.
1️⃣ builder 단계 과정
- 애플리케이션이 의존 모듈이 정의된 package.json 파일 복사
- npm install 명령르로 의존 모듈 다운
=> 컴파일이 필요하지 않아 빌드 과정은 끝이다.
2️⃣ Image build
cd ch04/exercises/access-log
docker image build -t access-log .
- 다른 서비스로부터 호출을 받아 로그를 남기는 REST API이다.
- HTTP POST를 통해 남길 로그 접수, GET으로 현재까지 기록된 로그 건수를 알려준다.
3️⃣ Container run
docker container run --name accesslog -d -p 801:80 --network nat access-log
http://localhost:801/stats에서 지금까지 남긴 로그 건수 확인할 수 있다.
4. 애플리케이션 빌드 예제: Go
📍Java와 Go의 차이
- Go는 nativa binary로 컴파일 되는 cross-platform language
- 어떤 플랫폼에서든 binary를 컴파일할 수 있다.
- java, Node.js와 달리 별도의 런타임 필요하지 x
- 애초에 도커 자체가 Go로 구현됐을 정도
FROM diamol/golang AS builder
COPY main.go .
RUN go build -o /server
# app
FROM diamol/base
ENV IMAGE_API_URL="http://iotd/image" \
ACCESS_API_URL="http://accesslog/access-log"
CMD ["/web/server"]
WORKDIR /web
COPY index.html .
COPY --from=builder /server .
RUN chmod +x server
- Dockerfile 스크립트의 각 빌드 단계는 서로 다른 기반 이미지 사용
- builder 단계는 diamol/golang 사용(Go 언어의 도구가 설치된)
- Application 단계는 diamol/base 사용(최소한의 os 레이어만을 포함)
1️⃣ Image build
cd ch04/exercises/image-gallery
docker image build -t image-gallery .
2️⃣ Image size 비교
docker image ls -f reference=diamol/golang -f reference=image-gallery
//diamol/golang 이미지가 왜 없지???!?!~!?~??~!?~?!?~?!?~?!
3️⃣ Container run
docker container run -d -p 802:80 --network nat image-gallery
http://localhost:802에 접속하면 NASA가 제공하는 오늘의 천문 사진을 볼 수 있다.
📢 세 개의 컨테이너에 걸쳐 실행되는 분산 애플리케이션
1. Go로 구현된 웹 애플리케이션이 Java로 구현된 API를 호출
2. 이미지의 상세 정보를 얻은 다음 Node.js로 구현된 API에 접근 로그를 남김
5. Multi-Stage Dockerfile 스크립트
📍 Multi-Stage 장점
1. 표준화
- 모든 빌드 과정이 도커 컨테이너 내부에서 이뤄져 어떤 환경에서도 같은 환경으로 작업할 수 있다.
- 컨테이너는 모든 도구를 정확한 버전으로 가지고 있다.
- 서버 관리 부담, 버전 차이로 인한 빌드 실패 줄일 수 있다.
2. 성능 향상
- multi-stage 빌드의 각 단계는 자신만의 캐시를 가진다.
- docker는 빌드 중에 각 인스트럭션에 해당하는 레이어 캐시를 찾아 사용한다.
- cache 재사용을 통해 90% 이상의 빌드 단계에서 시간 절약할 수 있다.
3. 최적화
- 빌드 과정을 세밀하게 조정하여 이미지의 크기를 가능한 작게 유지할 수 있다.
- 이미지에 불필요한 도구는 빼버릴 수 있다.
- 이미지 크기를 줄여 application의 시작 시간을 단축할 수 있다.
- 의존 모듈 자체를 줄여 취약점을 이용한 외부 공격의 가능성도 최대한 차단할 수 있다.
6. 연습 문제
Multi-Stage build와 Dockerfile 스크립트의 최적화를 연습해보자.
문제
- ch04/lab의 Dockerfile로 이미지 빌드, 이어서 Dockerfile 최적화한 다음 새로운 이미지 빌드하라.
- 최적화된 이미지 크기를 리눅스 환경 약 15MB, 윈도 환경에서 약 260MB가 되도록 하라.
- 현재 Dockerfil에 포함된 HTML 파일의 내용을 수정하면 일곱 단계의 빌드 단계 재수행한다.
- Dockerfile을 최적화해서 HTML 파일을 수정하더라도 재수행하는 빌드 단계가 한 단계가 되도록 하라.
📍최적화 전
1️⃣ Image build
FROM diamol/golang
WORKDIR web
COPY index.html .
COPY main.go .
RUN go build -o /web/server
RUN chmod +x /web/server
CMD ["/web/server"]
ENV USER=sixeyed
EXPOSE 80
docker image build -t ch04-lab-sol .
2️⃣ Container run
docker container run -d -p 802:80 ch04-lab-sol
📍최적화 후
1️⃣ Image build
FROM diamol/golang AS builder
COPY main.go .
RUN go build -o /server
RUN chmod +x /server
# app
FROM diamol/base
EXPOSE 80
CMD ["/web/server"]
ENV USER="sixeyed"
WORKDIR web
COPY --from=builder /server .
COPY index.html .
docker image build -t ch04-lab-sol-opt -f Dockerfile.optimized .
2️⃣ Container run
docker container run -d -p 804:80 ch04-lab-sol-opt
📍최적화 전과후 크기 비교
최적화된 ch04-lab-sol-opt의 SIZE가 16.4MB로 최적화하기 전인 736MB보다 줄은 것을 확인할 수 있다.