Docker

[Docker] 3. Application build

강철곰탱이 2023. 7. 8. 13:32
더보기

❗️이 글은 엘튼 스톤맨의 "도커 교과서" 교재를 참고하여 작성하였습니다.

 

목차
1. 빌드 서버가 필요할까?
2. 애플리케이션 빌드 예제: JAVA
3. 애플리케이션 빌드 예제: Node.js
4. 애플리케이션 빌드 예제: Go
5. Multi-Stage Dockerfile 스크립트
6. 연습 문제


1. 빌드 서버가 필요할까?

소프트웨어 프로젝트를 빌드하려면 개발 팀과 빌드 서버는 모든 도구를 같은 버전으로 사용해야 한다.

빌드 서버와 버전이 달라지는 것만으로도 빌드가 실패할 수 있기 때문이다.

 

도커를 사용하여 빌드 툴체인 한 번에 패키징,

  1. 개발에 필요한 도구 배포하는 Dockerfile 스크립트 작성 후 이미지로 만들기
  2. 이미지를 사용해 소스코드 컴파일함으로써 애플리케이션 패키징

📍멀티 스테이지 빌드 적용

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 파라미터를 이용해 이름을 붙일 수 있다.
  • 여러 단계로 나뉘어 있지만, 최종 산출물은 마지막 단계의 내용물을 담은 도커 이미지다.
  • 각 빌드 단계는 독립적으로 실행되지만, 앞 단계에서 만들어진 디렉터리나 파일을 복사할 수 있다.

이 예제에서는,

  1.  build-stage 단계에서 build.txt 파일을 생성
  2. (1)에서 생성된 파일을 test-stage로 복사하고 파일 생성
  3. (2)에서 생성된 파일을 복사

- Run 인스트럭션

: 빌드 중에 컨테이너 안에서 명령을 실행한 다음 그 결과를 이미지 레이어에 저장

  • FROM 인스트럭션에서 지정한 이미지에서 실행할 수 있는 것이어야 한다. (diamol/base)
  • diamol/base 이미지는 echo 명령을 포함하기 때문에 정상적으로 작동

📍 멀티 스테이지 빌드 적용한 dockerfile 스크립트 실행 과정

  1. 1단계 빌드에서 텍스트 파일 생성
  2. (1)에서 생성한 텍스트 파일 복사하고 파일 생성
  3. (2)에서 생성한 텍스트 파일 복사

✂️ 주의

  • 각 빌드 단계는 서로 격리
  • 빌드 단계별로 기반 이미지가 달라 사용할 수 있는 도구도 다를 수 있다.
  • 한 단계에서라도 명령이 실패하면 전체 빌드가 실패한다.

📍 멀티 스테이지로 이미지 빌드하기

멀티 스테이지 Dockerfile 스크립트로 자바 애플리케이션 빌드해보자.

cd ch04/exercises/multi-stage 
docker image build -t multi-stage .

Dockerfile 스크립트에 작성된 순서에 따라 빌드가 진행된다.

  1. [build-stage] 단계에서 build.txt 파일 생성
  2. [test-stage] 단계에서 build.txt 파일을 이미지에 추가
  3. 최종 결과물이 될 이미지 생성 => [test-stage ]단계에서 build.txt 파일 복사해온다.

📍자바 애플리케이션의 멀티 스테이지 빌드 과정

  1. 의존 모듈을 내려받고 애플리케이션을 빌드
  2. 빌드 결과물을 복사해 온 다음 테스트 수행
  3. 테스트가 끝난 빌드 결과물을 복사

=> 이런 방법으로 애플리케이션의 '이식성'을 확보할 수 있다. 

 

도커만 갖춰진다면,

  • 컨테이너를 통해 어떤 환경에서든 애플리케이션 빌드/실행 가능
  • 새로 팀에 합류한 개발자 즉시 개발 환경 갖출 수 있다.
  • 빌드 도구를 도커 이미지를 통해 중앙 집중적으로 관리 가능

 


2. 애플리케이션 빌드 예제: JAVA

 

자바 스프링부트를 사용해 구현한 것을 예제로 빌드하고 실행하는 과정을 이해해보자.

 

애플리케이션을 빌드하고 실행하기 위한 빌드 도구 모두 도커 이미지를 통해 가져온다.

애플리케이션은 자바 빌드 도구로 MavenOpenJDK를 사용한다.

  • 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 단계에서 하는 일

  1. 기반 이미지는 diamol/maven
    • 이미지는 maven과 OpenJDK 포함.
  2. 이미지에 작업 디렉터리 만든 다음 이 디렉터리에 pom.xml 파일을 복사
    • pom.xml 파일에는 maven에 수행할 빌드 절차 정의되어 있음.
  3. 첫 번째 RUN 인스트럭션에서 maven이 실행돼 필요한 의존 모듈 다운
    • 시간이 오래 걸려 별도의 단계로 분리해 레이어 캐시 활용할 수 있도록 함.
    • 새로운 의존 모듈이 추가된 경우((XML 파일 변경됐을 것)), 이 단계 다시 실행
    • 새로운 의존 모듈 추가되지 않은 경우 : 이미지 캐시 재사용
  4. COPY 인스트럭션 통해 나머지 소스 코드 복사
    • "실행중인 디렉터리에 포함된 모든 파일과 서브 디렉터리를 현재 이미지 내 작업 디렉터리로 복사"하라는 의미
  5. 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 포맷으로 확인 가능

http://localhost:800/image

✏️ 도커가 설치된 경우 => 소스 코드와 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 단계 과정

  1. 애플리케이션이 의존 모듈이 정의된 package.json 파일 복사
  2. 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에서 지금까지 남긴 로그 건수 확인할 수 있다.

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보다 줄은 것을 확인할 수 있다.