이미지가 어떻게 저장되는지 알아보고, 이미지를 효율적으로 저장하는 레이어드 구조에 대해서 알아보자. 이미지가 저장되는 원리를 알아보며, 이미지를 만드는 방법인 커밋과 빌드 두 가지 방식을 알아보자.
또한, 이미지를 빌드하려면 도커파일이라는 명세서를 작성해야 하는데, 도커파일을 작성하기 위해 필요한 문법들을 알아보고, 실제로 소스 코드를 애플리케이션 이미지로 빌드하자.
마지막으로 더 효율적인 빌드 방식인 멀티 스테이징을 알아보자.
이미지는 컨테이너를 실행하기 위한 읽기 전용 파일이다.
도커 이미지는 저장소를 효율적으로 사용하기 위해 레이어드 파일 시스템으로 구성되어있다. 레이어는 하나의 층을 의미하며, 여러 개의 층으로 구성되어 있는 것에서 하나의 층을 레이어라고 표현한다.
이미지는 여러 개의 레이어로 구성되어 있다.
각각의 레이어는 이미지의 일부분을 나타낸다.
이를 위해 건축도면의 예시로 이해해보자. (똑같은 것은 아니지만 유사하다.)
→ 수정할 때 전체 도면이 영향을 받지 않고, 레이어마다 변경해주면 된다. 즉, 변경사항에 있어 재활용이 유리한 구조이다.
→ 겹치는 레이어를 재사용할 수 있다. 사용량도 줄이고, 보관하기에도 효율적이다. 즉, 데이터 전송에도 유리한 구조이다.
이미지 레이어는 바로 직전 단계의 레이어에서 변경된 내용들만 저장된다.
서버의 간단한 페이지를 출력하는 Nginx를 설치한다 해보자. Nginx를 구성하는 단계는 먼저 OS를 설치하고 Nginx 소프트웨어를 설치 한 뒤, Nginx 설정 파일을 수정하고 브라우저에 표시되는 index.html 파일을 사용자에게 응답할 내용으로 수정해야 한다.
이 순서대로 이미지 레이어를 구성한다고 생각해보면 먼저 OS를 준비한 다음에 이 OS위에서 Nginx 소프트웨어를 설치한다. 소프트웨어를 설치하면 OS의 특정 폴더에 Nginx 소프트웨어 관련 파일들이 추가된다. 그래서 Nginx를 설치한다는 것은 기존 OS 파일 시스템에서 추가가 되는 부분이 있는 것이다. 기존 레이어에서 변경되는 것들은 기존 레이어를 수정하는 것이 아니라, 기존 레이터 위에 변경된 내용들이 새로운 레이어로 저장된다.
그래서 두번째 Nginx 설치 레이어에는 이전 레이어인 OS 레이어에서 Nginx 소프트웨어가 추가된 부분만 따로 가지고 있다.
→ 이전 레이어와 비교해서 추가되거나 변경된 파일들이 다음 레이어로 저장된다. 이미지에서 한 번 저장된 레이어는 변경할 수 없다. 따라서 변경 사항이 있다면 새로운 레이어로 저장해야 한다.
Nginx 설정 파일인 nginx.conf파일을 작성하나거나 index.html 파일의 내용을 수정하는 것도 각각 새로운 레이어로 만들어 진다.
ImageA를 완성한 뒤 이전과 똑같은 순서지만 마지막에서 index.html 파일의 내용을 custom한 ImageB를 만든다면, 3.Nginx 설정까지는 A,B 모두 같은 레이어를 재사용할 수 있다. 마지막만 다르기 때문에 마지막 레이어만 새롭게 추가되어 ImageB가 완성된다.
따라서 ImageA와 B는 총 3개의 레이어를 공유하고, 하나의 레이어를 별도로 사용하는 구조가 된다.
→ 이미지의 레이어는 순차적으로 쌓이고, 각각의 레이어는 이전 레이어에서 변경된 부분을 저장한다. 같은 변경이 일어난 레이어는 공유해서 재사용할 수 있다.
docker run 으로 컨테이너 실행 시, 이미지의 가장 마지막 레이어 위에서 새로운 읽기, 쓰기 전용 레이어인 ‘컨테이너 레이어’가 추가된다.
애플리케이션에서 로그가 쌓이거나 컨테이너가 실행 중에 생기는 모든 변경 사항들은 이 새로운 레이어에 저장된다.
이미지의 레이어는 변경 불가능하기에 수정 불가능한 읽기 전용 레이어이다. 이미지 레이어는 컨테이너를 실행하기 위한 세이브 포인트 역할을 한다.
컨테이너 레이어는 실제로 이 이미지를 컨테이너로 실행한 다음에 프로세스가 변경하는 내용을 기록하는 레이어이다. 컨테이너 레이어는 읽기 쓰기 모두 가능하기 때문에 컨테이너 레이어 한 장이 읽기 전용 레이어들인 이미지의 상단에 추가돼서 컨테이너 실행 중에 변경되는 내용만 기록하는 것이다.
ImageA로 Container2를 실행하면 컨테이너에 읽기, 쓰기 레이어가 생성된다.
Container1,2는 이미지가 동일하기 때문에 동일한 읽기 전용 레이어를 공유한다.
→ 실제 컨테이너를 실행할 때 전체 레이어를 복사하는 것이 아닌 읽기 전용 레이어인 이미지 위에 새로운 컨테이너 레이어만 하나씩 추가하는 것이다. (실제로 하나의 이미지를 공유해서 읽어오는 것이다.)
Container3의 경우 세번째 레이어까지만 공유한다. 따라서 이 컨테이너로 접근하면 B의 index.html 파일의 내용인 custom index가 출력된다.
→ 모든 컨테이너는 각자 자기만의 읽기 쓰기 레이어 한장을 가진다. 컨테이너를 만들 때 사용된 이미지에 따라서 이미지의 읽기 전용 레이어 전체를 공유할 수도, 일부만 공유할 수도 있다.
→ 이미지의 읽기전용 레이어를 활용하면 컨테이너를 실행할 때 전체 공간을 복사하지 않아도 돼서 빠르게 컨테이너를 실행할 수 있다.
→ 컨테이너가 늘어나면서 사용하는 공간을 최대한 작게 관리할 수 있다.
docker image history 이미지명
해당 명령어를 통해 3개의 이미지 레이어 이력을 조회하고, 레이어의 특징을 알아보자.
→ hello-nginx는 Nginx 오피셜 이미지 다운로드 후 index.html을 hello-nginx가 있는 파일로 덮어쓰게 했다.(COPY: 내가 가지고 있는 파일을 기존 레이어로 붙여넣기하는 방법)
→ 이전과 유사한데 한 줄 추가됐다. config-nginx의 경우 index.html을 덮어쓰기하고, 그 다음에 nginx.conf(nignx의 서버 설정 가능 파일)도 덮어쓰기 했다.
→ 이전과 비슷하지만 순서가 다르다. pre-config-nginx이미지에선 nginx.conf를 먼저 수정하고, index.html를 수정했다.
docker image inspect 이미지명
→ 다음을 통해 이미지의 메타데이터를 확인해보면 “Layers”를 확인할 수 있다. 이때 각 레이어의 고유 해시값을 알 수 있는데 해시값은 SHA256 알고리즘으로 생성되고, 각각의 레이어의 변경사항을 암호화한 값이다.
→ 이미지 레이어의 고유한 해시 값을 통해 다른 레이어와 구분할 수 있다. 즉, 레이어마다 고유한 ID값이 있다.
→ 이미지의 레이어는 이전 파일에서 변경된 사항을 저장하기 때문에 결과적으로 두 개가 같은 내용이더라도 구성한 순서가 다르면 완전히 다른 레이어로 구성된다.
Layering: 각 레이어는 이전 레이어 위에 쌓이며, 여러 이미지 간에 공유될 수 있다. 이미지는 레이어 방식을 통해 중복된 데이터를 최소화하고, 빌드 속도를 높이며, 저장소를 효율적으로 관리한다.
Copy-on-Write(CoW)전략: 다음 레이어에서 이전 레이어의 특정 파일을 수정 할 때, 해당 파일의 복사본을 만들어서 변경 사항을 적용한다. 따라서 원래 레이어는 수정되지 않고, 그대로 유지된다. 새로운 레이어는 이전 레이어에 영향을 주지 않고, 자신의 레이어에 수정할 내용을 카피하고 수정해서 사용한다.
→ 읽기 쓰기 전용인 컨테이너 레이어를 생각하면 된다. 실행된 컨테이너에서 변경된 모든 것은 이미지가 아닌 컨테이너 레이어에 저장된다.
→ nginx 컨테이너를 실행한 뒤 컨테이너 안에서 index.html를 수정한다 하면, 이미지 위에 있는 index.html 파일의 내용을 수정하는 것이 아니라, 컨테이너 레이어에 수정할 index.html 파일을 복사(Copy)해 온 다음 이 파일을 수정(Write)한다.
→ 이때 Layer에서 index.html 파일은 총 3개가 되며, 이 중 가장 최근에 변경한 index.html이 실제 파일의 내용으로 사용된다.
이미지의 커밋 방식을 알아보고 커밋을 통해 이미지를 만들어 보자.
→ 대부분 빌드 방식을 사용하지만 빌드 방식이 커밋을 기반으로 동작하기에 커밋방식도 알아두는 것이 좋다.
docker run -it --name 컨테이너명 이미지명 bin/bash
→ 이미지 내부의 파일 시스템을 확인해보거나 디버깅하는 용도로 많이 사용되는 방법이다.
docker commit -m 커밋명 실행중인컨테이너명 생성할이미지명
(1),(2)는 각각의 터미널을 의미한다.
→ 커밋 방식을 사용하면 기존 레이어에 새로운 레이어를 한장 더 추가할 수 있다. 이런 식으로 컨테이너의 이미지는 기존 이미지의 레이어에 새로운 레이어를 쌓아가면서 이미지를 만드는 것이다.
기업에서 휴먼에러(사람이 직접 인프라를 컨트롤 하다 보면 실수가 나올 수 있는 것들을 의미한다.) 가 발생하기도 하며, 개인이 인프라를 관리하면 인프라의 상태를 변경한 기록들을 관리하기 쉽지 않다.
그래서 IaC 방법을 사용해 코드로 인프라의 상태를 관리할 수 있다.
코드에 상세 작업 내용이 기재되어 있고, 이 작업을 사람이 아닌 프로그램이 대신 수행해 주는 것이다. 프로그램이 작업을 하기 때문에 작업들을 더 빠르고 안전하게 실행할 수 있다.
코드에 들어가는 내용은 프로그램이 작업하기 위한 일련의 작업 명세서로 사용된다. 이런 명세서를 Github 같은 소스 코드 레포지터리에 저장하면 인프라의 상태도 소스코드처럼 버전관리를 할 수 있다.
IaC 방법론을 활용하는 많은 소프트웨어들이 시중에 있고, Docker도 그 중 하나이다.
Docker는 Dockerfile이라는 소스코드를 사용해서 인프라의 상태를 저장하는 이미지를 만들 수 있다.
→ 복잡하기 때문에 사람이 직접 작업하면 문제가 발생할 가능성이 크다.
이미지 제작자가 도커가 이해할 수 있는 문법에 따라 도커 파일을 작성하면, 도커는 임시로 컨테이너를 실행하고 사용자가 정의한 작업을 수행한 뒤 커밋을 실행한다.
따라서 도커 빌드를 활용하면 여러 개의 레이어도 쉽게 추가할 수 있다.
레이어 한 장을 추가한 다음에 다시 그 레이어로 만든 이미지를 컨테이너로 만들고, 그 컨테이너 위에서 변경 작업을 한 뒤 추가로 커밋하는 것을 도커가 알아서 반복해준다.
→ 도커는 IaC 방식에 따라 이미지를 도커 파일이라는 소스코드로 관리할 수 있다. 코드이기 때문에 애플리케이션 소스 코드와도 함께 관리할 수 있고, 버전 관리도 할 수 있다.
→ 도커파일에는 이미지를 어떻게 만드는지에 대한 세부 내용이 기재되어 있고, 도커는 이 도커파일을 해석해서 이미지를 제작해준다.
이미지 빌드는 도커에서 가장 많이 사용되는 기능 중 하나로, 컨테이너 애플리케이션 개발에 있어서 가장 핵심 기능이라 볼 수 있다.
이미지는 도커 빌드 명령어를 통해 빌드할 수 있다.
docker build -t 이미지명 Dockerfile경로
: 도커 파일 문법은 지시어와 지시어의 옵션으로 구성된다.
커밋은 사용자가 직접 새로운 이미지를 만드는 방법이다.
빌드는 도커 데몬이 도커 파일에 적힌 지시어를 사용해서 이미지를 자동으로 만들어주는 방법이다.
도커 빌드는 IaC 개념으로 인프라의 상태를 도커 파일이라는 코드로 관리할 수 있다. 커밋 빌드에서 사용자가 직접 수행했던 일들을 Dockerfile의 지시어로 기록해놓고 필요할 때마다 도커 데몬에게 이미지 빌드를 맡긴다.
하나의 커밋은 기존 레이어에 하나의 새로운 레이어를 추가하며, 여러 레이어를 쌓으려면 커밋을 완료한 이미지를 새로운 컨테이너로 만들고 다시 두번째 레이어를 커밋하는 순서대로 여러 차례 커밋을 수행해야 한다. but Dockerfile을 사용하면 도커가 직접 이 작업들을 반복해주기 때문에 여러 개의 레이어 구조를 편리하게 활용할 수 있다.
이미지를 빌드할 때 사용되는 폴더이다.
이미지 빌드 방식은 도커 데몬이 임시 컨테이너를 실행시키면서 레이어드를 하나씩 추가한다. 그래서 도커 데몬에게 도커 파일과 빌드에 사용되는 파일들을 전달해줘야 한다.
이렇게 도커 데몬에게 전달해주는 폴더를 빌드 컨텍스트라고 한다.
(이전 실습에선 Dockerfile을 작성한 01.buildnginx 폴더가 빌드 컨텍스트이다.)
도커 빌드 명령 시 빌드 컨텍스트가 도커 데몬에게 통째로 전달된다.
그래서 이 컨텍스트 안에 있는 도커 파일로 도커 데몬이 이미지를 빌드하는 것이다. 그리고 도커 파일에서 COPY 지시어를 사용하면 빌드 컨텍스트에 있는 파일이 빌드에 사용되는 컨테이너로 복사된다.
도커 데몬은 빌드 컨텍스트에 있는 파일만 COPY 명령으로 복사할 수 있다.
→ 빌드 컨텍스트란, 도커 데몬이 이미지를 빌드할 때 전달되는 폴더이고, 이 폴더 안에 도커 파일과 COPY에 사용할 파일들이 모두 있어야 한다.
빌드 컨텍스트의 .dockerignore 파일을 통해 빌드 컨텍스트로 전달될 파일을 관리할 수 있다. 예를 들어 이미지 빌드에 필요하지 않은 3GB 크기의 파일이 있다면, 이런 라지정크를 도커 데몬에 전달하는 것은 비효율적이다. 따라서 이 파일명을 .dockerignore 파일에 적어 놓으면 이를 제외하고 전달할 수 있다.
빌드 컨텍스트는 도커 데모에게 전달돼야 하기 때문에 빌드 컨텍스트의 크기가 커질수록 전송시간이 길어지고, 폴더의 크기가 비정상적으로 커지면 빌드에 문제가 생길 수 있다. 그래서 도커 파일과 빌드에 사용되는 파일만 별도의 폴더로 관리해야 한다.
Node.js로 개발된 소스코드를 애플리케이션으로 빌드하려면,
1. Node.js가 설치된 OS 환경이 필요하다.
2. Node.js 개발된 소스 코드가 필요하다. 보통 이 코드는 Git을 통해 다운 받는다.
3. 의존 라이브러리를 설치(application build)해야한다. (여기선 npm install)
4. 애플리케이션을 실행시킨다. (npm start)
→ 도커 이미지를 빌드할 때 일반적인 소프트웨어만 실행할 때는 실행 환경을 준비하고 실행할 프로그램만 준비하면 된다. 하지만 개발한 소스코드를 이미지로 빌드하려면 먼저 소스 코드 애플리케이션으로 빌드하고, 그 애플리케이션을 실행할 수 있는 환경을 준비하면 된다. 따라서 후자의 경우엔 컨테이너 이미지를 빌드하는 과정 안에서 애플리케이션 빌드 과정이 포함되어 있다.
FROM 이미지명
COPY 빌드컨텍스트경로 레이어경로
RUN 명령어
CMD["명령어"]
→ 새로운 레이어를 추가하는 지시어와 추가하지 않는 지시어가 있다. 파일 시스템의 내용을 변경하는 부분이 있으면 일반적으로 레이어를 추가하고, 메타데이터에만 영향을 주는 부분은 레이어가 추가되지 않는다. 그래서 지시어들을 잘 활용해서 이미지의 레이어 개수를 관리할 수 있다.
docker build -f 도커파일명 -t 이미지명 Dockerfile 경로
→ 이렇게 도커를 활용하면 Node.js가 컴퓨터에 설치되어 있지 않아도 컨테이너 이미지를 통해 Node.js 애플리케이션을 빌드하고 실행할 수 있다.
WORKDIR 폴더명
USER 유저명
EXPOSE 포트번호
ARG 변수명 변수값
ENV 변수명 변수값
→ 시스템에서 환경변수는 다양한 방법으로 활용할 수 있다.(ex. 환경변수에 따라 다른 색깔의 웹페이지, DB에 접속하기 위해 DB접속 정보를 환경변수에 저장..)
→ ARG와 ENV의 차이는 컨테이너를 실행할 때 환경변수가 유지되는지이다.
→ 기본적으로 특수한 경우를 제외하고는 애플리케이션에서 환경변수를 설정하고 싶으면 ENV로 설정해주면 된다.
ENTRYPOINT["명령어"]
CMD["명령어"]
docker build -f Dockerfile-entrypoint -t buildapp:entrypoint .
→ Dockerfile-entrypoint 도커파일을 사용해 실습 이미지 빌드
docker run --name buildapp-entrypoint-list buildapp:entrypoint list
→ buildapp:entrypoint 이미지를 CMD를 list로 덮어씌우며 컨테이너로 실행
→ 실제 entrypoint에 npm이 있기때문에 실제로 실행되는 것은 npm list가 실행 될 것이다.
docker run -it --name buildapp-entrypoint-bash buildapp:entrypoint /bin/bash
→ bash로도 접근, 원래 이렇게 덮어쓰기하면 shell로 접근되었는데 엔트리 포인트에 npm이라는 명령어가 있기 때문에 실제 실행되는 명령어는 npm /bin/bash가 실행돼서 에러가 발생한다.
→ ENTRYPOINT를 사용해 사용자가 의도하지 않은 명령어로 접근하는 것을 1차적으로 막을 수 있다. (but ENTRYPOINT도 ENV처럼 컨테이너로 실행할 때 덮어쓰기가 가능하기 때문에 100% 보안적으로 안전하진 않다.)
→ 도커 파일에서 두 개의 베이스 이미지를 활용하는 방법이다.
보통 애플리케이션을 빌드하는 과정에서 만들어지는 파일들이 용량을 많이 차지한다. 이 파일들은 실제로 애플리케이션이 실행되는데 사용되지 않기 때문에 이미지를 빌드에 사용하는 이미지와 실행에 사용하는 이미지로 나누는 것이다.
→ 장점은 애플리케이션이 실행되는 이미지의 크기를 줄일 수 있다.
# 빌드 환경 설정
FROM maven:3.6-jdk-11
WORKDIR /app
# pom.xml과 src/ 디렉토리 복사
COPY pom.xml .
COPY src ./src
# 애플리케이션 빌드(jar 파일 생성)
RUN mvn clean package
# 빌드된 JAR 파일을 실행 환경으로 복사
RUN cp /app/target/*.jar ./app.jar
# 애플리케이션 실행
EXPOSE 8080
CMD ["java", "-jar", "app.jar"]
이미지 빌드 과정을 두가지로 나눠보자.
→ 애플리케이션 빌드 & 애플리케이션 실행
→ 첫번째 단계에서는 소스코드를 복사한 뒤, mvn 패키지 명령을 통해 jar 파일을 만든 후, 실제 애플리케이션을 실행하는 이미지에는 Java Runtime만 있는 이미지를 가져온 다음 앞선 결과의 jar파일만 복사해와서 애플리케이션 실행에 사용한다.
빌드 스테이지와 실행 스테이지. 두개를 나눠서 빌드하는 방식이 멀티 스테이지 빌드이다.
# 첫번째 단계: 빌드 환경 설정
FROM maven:3.6 AS build
WORKDIR /app
# pom.xml과 src/ 디렉토리 복사
COPY pom.xml .
COPY src ./src
# 애플리케이션 빌드
RUN mvn clean package
# 두번째 단계: 실행 환경 설정
FROM openjdk:11-jre-slim
WORKDIR /app
# 빌드 단계에서 생성된 JAR 파일 복사
COPY --from=build /app/target/*.jar ./app.jar
# 애플리케이션 실행
EXPOSE 8080
CMD ["java", "-jar", "app.jar"]
→ 첫 번째 컨테이너는 maven 컨테이너이고, 이 컨테이너를 통해 만들어낸 jar 파일을 jdk 컨테이너에서 복사해 와서 사용하는 것이다.
→ 멀티 스테이징 기술을 잘 활용하면 애플리케이션의 이미지 크기를 크게 줄일 수 있다.
도커마스터의 도커이미지 멀티빌드.. 나중에 도커어플리케이션 배포할때 써보겠습니다
멀티빌드..메모...