Intro
자동 배포를 공부하니 정말 알아야 할 것들이 산더미다. 그중에서도 핵심적인 부분이 프로젝트를 JAR 파일로 빌드하여 이미지로 생성하고, 그것을 가상의 컴퓨터에서 실행시키는 것인데, 자동 배포를 구글링을 하여 어찌어찌 성공했으나 이미지 빌드로 Jib 라이브러리를 사용하니 그 과정에 대한 부분은 전혀 모른 채 사용하게 됐다.
어차피 회사마다 CI/CD의 의미가 다르다고 하니 도커 이미지 빌드 과정도 배울 겸 오늘은 dockerfile에 대해서 글을 써보고자 한다.
Dockerfile 이란?
Dockerfile이란 도커 이미지를 만들기 위한 스크립트 파일이다. 내부에는 도커 이미지를 만들기 위한 명령어들이 순서대로 기술되어 있다. Dockerfile을 이용하지 않고 도커 이미지를 만들 수 있는 방법이 있다고는 하지만 그것은 일반적인 방법이 아니기에 Dockerfile로 이미지를 생성해야 한다. Dockerfile의 내부에는 여러 명령어들이 들어갈 수 있는데 주요한 도커 파일 명령어에 대해 설명해 보겠다.
Dockerfile 명령어
FROM eclipse-temurin:17-jdk-jammy AS builder
COPY gradlew ./
COPY gradle ./gradle/
COPY build.gradle ./
COPY settings.gradle ./
COPY src ./src/
RUN chmod +x ./gradlew
RUN ./gradlew bootJar
FROM eclipse-temurin:17-jdk-jammy
COPY --from=builder build/libs/*.jar /app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]
VOLUME /tmp
From "도커 제공 베이스 이미지"
먼저 도커 이미지를 빌드할 때 기본이 되는 베이스 이미지가 필요하다. 여기서 베이스 이미지란 새로 생성될 도커 이미지의 기본 환경을 뜻한다. 해당 도커 이미지의 운영체제쯤으로 생각하면 될 듯싶다.
베이스 이미지의 경우 도커에서 다양하게 지원하고 있다. 해당 프로젝트는 자바언어와 스프링 부트로 만들어진 프로젝트이기에 jdk가 필수이므로 eclipse-temurin:17-jdk-jammy를 사용하였다. 첫 번째 FROM 명령어 끝부분에 AS builder라는 별칭을 선언하는 부분이 있는데, 왜 별칭을 선언했는지는 아래쪽에서 자세히 설명하겠다.
COPY "복사해 올 파일의 경로" "도커 작업 디렉토리 경로"
COPY는 원하는 파일을 원하는 경로에 복사할 수 있는 명령어이다. 해당 명령어를 사용하며 궁금했던 부분이 위쪽 코드에서 보면 gradlew, build.gradle, setting.gradle은 작업 디렉토리 경로가 './' 인데 gradle과 src는 경로가 달랐다.(./gradle/ , ./src/) 사실 이렇게 나눠놓고 보니 이유가 예상이 됐지만, 확실하게 알고 싶어서 gpt에게 문의했다.
문의해 보니 gradlew, build.gradle, setting.gradle은 '파일' 이기 때문에 그 자체만을 복사해 오면 된다. 따라서 붙여 넣기 할 때도./ 경로만 표시해 주면 현재 위치한 작업 디렉토리에 파일을 복사해 올 수 있다. 하지만 gradle과 src는 파일이 아닌 '디렉토리' 이므로 해당 디렉토리의 하위 내용까지 복사해와야 한다. 따라서 작업 디렉토리 경로를 적을 때 해당 디렉토리들(gradle, src)의 이름까지도 적어줘야 한다. (./gradle/ , ./src/ 요렇게) 만약 저렇게 적지 않는다면 내용물이 없는 디렉토리만 복사해온다고 한다.
RUN "명령어"
RUN 명령어는 Docker 이미지를 빌드할 때 새로운 레이어에서 명령을 실행하는 데 사용된다. 이 명령어는 도커 이미지를 생성할 때 실행할 명령을 정의하는 데 사용된다.
위의 스크립트를 살펴보면 RUN chmod +x ./gradlew는 gradlew에 접근 가능하도록 권한을 부여하는 명령어이고, RUN ./gradlew bootJar 부분은 접근이 허용됐으니 gradlew 스크립트를 실행시켜 JAR 파일을 생성하는 부분이다.
ENTRYPOINT [ "컨테이너 실행 시 어떤 것을 실행할 것인지", "인수 1", "인수 2",...]
ENTRYPOINT는 도커 이미지가 실행될 때 실행되는 명령이나 스크립트를 지정하는 데 사용되는 Dockerfile 지시어다. CMD와 마찬가지로 컨테이너를 시작할 때 실행되는 명령을 지정하지만, ENTRYPOINT는 컨테이너가 시작될 때 CMD로 지정된 명령어나 인수를 덮어쓰지 않고 뒤에 추가한다.
ENTRYPOINT 지시어 중 첫 번째로 오는 부분은 컨테이너 실행 시에 어떤 것을 실행할 것인지를 선언하는 부분이다. 첫 번째 선언부 이후로는 컨테이너가 시작될 때 실행될 명령어에 전달될 인수들을 나타낸다. 위의 코드로 예시를 들어 종합해 보자면 java 애플리케이션을 실행하는데 JAR 파일로 실행할 것이고, 그 위치는 /app.jar에 있다는 의미이다.
VOLUME "경로"
VOLUME은 호스트 시스템의 '/tmp' 디렉토리를 컨테이너 내부의 '/tmp' 디렉토리와 연결하는 역할을 한다. 컨테이너 내부의 데이터를 호스트 시스템에 영속적으로 저장하도록 하는 데 사용된다. 아무래도 도커 컨테이너의 경우 실행 중인 동안에 쌓인 데이터들이 컨테이너가 종료됐을 때에 사라지는 비영속적 데이터 이기 때문에 영속적으로 저장할 수 있도록 설정하는 부분인 것 같다.
빌드 프로세스 기술 Multi-stage build
FROM eclipse-temurin:17-jdk-jammy AS builder
COPY gradlew ./
COPY gradle ./gradle/
COPY build.gradle ./
COPY settings.gradle ./
COPY src ./src/
RUN chmod +x ./gradlew
RUN ./gradlew bootJar
FROM eclipse-temurin:17-jdk-jammy
COPY --from=builder build/libs/*.jar /app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]
VOLUME /tmp
위의 코드를 살펴보면 FROM명령어 부분이 두 번 들어가 있다. FROM의 경우 해당 이미지의 베이스 이미지를 설정하는 명령어라고 했는데 왜 두 번이나 똑같은 명령어가 들어갈까라는 의문이 생겼다. 심지어 첫 번째 선언된 FROM의 경우에는 별칭까지 있다.
개념이 제대로 안 잡힌 상태에서 위의 코드를 분석했을 때는 어떠한 한 공간 안에 운영체제를 두 번 다운로드(?) 하는 것이니 초기화하는 건가? 싶었다. 근데 그 아래의 COPY --from=builder build/libs/*.jar /app.jar 이 명령어 부분을 보면 또 그건 아닌 것 같다는 생각이 들었다. 뭔가 별칭을 만들고, 그 별칭에서부터 파일을 가져온다? 그렇다면 한 공간이 아닌 두 개의 공간이 만들어질 것 같다는 추측이 들었다.
이러한 생각을 기저로 서칭을 하기 시작했지만 그 내용들은 공간이 어떻게 둘로 나뉜다는 건지에 대한 정확한 부분이 빠져있었다. 끈질기게 gpt를 물고 늘어진 결과 도커 자체에서 이미지를 빌드하기 위한 공간(스테이지)이 두 개가 생기는 것이고 각각의 공간에서 이미지를 만들어 낸다는 것을 알아냈다.
위의 코드로 얘기하자면 첫 번째 명령어의 FROM의 공간에서는 로컬의 디렉토리에서 필요한 파일들을 COPY하여 해당 작업 디렉토리에서 JAR 파일을 만드는 과정이다. 그렇게 해서 첫 번째 이미지가 생성된다.
두 번째 FROM은 첫번째 FROM에서 별칭으로 생성된 이미지에서 핵심 파일만 가져오는 역할을 한다. 그리고 그 파일을 실제로 컨테이너 화 시켰을 때 실행할 수 있게 하는 이미지이다. 최종적으로는 두 번째 FROM의 이미지가 빌드되는 것이다.
그렇다면 왜 Multi-stage build라는 빌드 프로세스 기술을 사용해야 할까? 로컬에서 JAR를 생성하여 그것을 바로 이미지화시키면 안 되는 것일까?
그 이유는 로컬에서 JAR 파일을 빌드하게 되면 빌드 파일(garadlew, build.gradle, settings.gradle)과 빌드 도구(gradle)가 포함된 채로 JAR 파일이 형성된다고 한다. 하지만 Multi-stage build라는 빌드 프로세스 기술을 사용하게 되면 불필요한 빌드 파일과 도구들을 제외한 핵심 JAR 파일만을 획득할 수 있다. 이미지의 경우 그 크기가 최소화될수록 더 가볍고 빠르기 때문에 불필요한 것들을 최소화하기 위해 위와 같은 기술을 사용한다.
Dockerfile로 도커 이미지 빌드 시에 발생한 에러 해결 과정
1. Docker login
ERROR: error during connect: this error may indicate that the docker daemon is not running: Get "http://%2F%2F.%2Fpipe%2Fdocker_engine/_ping": open //./pipe/docker_engine: The system cannot find the file specified.
간단한 오류긴 하지만 그래도 써본다. 도커 데몬이 실행되고 있지 않음을 나타내는 에러이다. 도커 로그인을 하지 않고 도커 관련 명령어를 사용할 경우 저런 오류가 난다.
2. exit code : 127
FROM eclipse-temurin:17-jdk-jammy AS builder
COPY gradlew ./
COPY gradle ./gradle/
COPY build.gradle ./
COPY settings.gradle ./
COPY src ./src/
RUN chmod +x ./gradlew
RUN ./gradlew bootJar
FROM eclipse-temurin:17-jdk-jammy
COPY --from=builder build/libs/*.jar /app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]
VOLUME /tmp
$ docker build .
[+] Building 2.4s (11/13) docker:default
=> [internal] load .dockerignore 0.0s
[+] Building 2.5s (12/13) docker:default
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 1.59kB 0.0s
=> [internal] load metadata for docker.io/library/eclipse-temurin:17-jdk-jammy 1.1s
=> [internal] load build context 0.1s
=> => transferring context: 20.79kB 0.1s
=> [stage-1 1/2] FROM docker.io/library/eclipse-temurin:17-jdk-jammy@sha256: 0.0s
57 | RUN chmod +x ./gradlew
58 | >>> RUN ./gradlew bootJar
59 |
60 | FROM eclipse-temurin:17-jdk-jammy
--------------------
ERROR: failed to solve: process "/bin/sh -c ./gradlew bootJar" did not complete successfully: exit code: 127
문제의 코드이다.(사실 코드 문제는 아니었다. 하지만 이때는 코드 문제인 줄...ㅎ)
아무리 봐도 이상이 없는데 RUN ./gradlew bootJar 이 명령어 부분에서 자꾸만 오류가 났다. 해당 에러 메시지를 확인해 보면 JAR 파일로 빌드하는 명령어 부분에서 자꾸만 오류가 났다. 127 코드가 어떤 것인지를 확인해 보니 '컨테이너의 커맨드가 존재하지 않는 경우'라고 한다.(이 부분 때문에 더 헷갈림)
그래서 처음에는 'COPY 경로가 잘못 설정됐나?' 싶어서 '/app'이라는 디렉토리를 만들어서 필요한 파일들을 넣어보았지만 소용이 없었다. 그렇다면 전체를 다 복사해오자 싶어서 'COPY . .' 명령어를 사용해 보아도 마찬가지였다. 일단 COPY의 문제는 아닌 거 같고...
두 번째로는 '빌드하는 부분에서 오류가 난다면 Gradle의 문제일까?'라고 생각해서 Gradle을 직접 이미지 내부에 깔아보기도 하고 했지만 소용이 없었다. 이것도 아닌 것 같다.
세 번째로는 혹시 내 컴이 윈도우여서...?(ㅇㅇ 맞음)
'윈도우라 COPY 경로를 못 읽나!!!' 싶었다.(아님) 그래서 COPY 경로를 절대 경로로 바꿔보고 별짓을 다했으나 소용이 없었다. 이것도 아니었다.ㅠ
마지막 드디어 이유를 찾아냈다. 구글에 에러 코드를 전체로 검색해 보니 생각보나 나 같은 사람들이 많았다.(진작에 검색해 볼걸 후회함)
진작에 검색해보지 못한 이유가 127 에러코드를 검색하여 그 이유를 찾은 후부터 그 내용에 꽂혀서 전체 검색은 해보지 못한 것이 문제였다. 그래도 쬐끔은 뿌듯했던 건 '내 컴퓨터가 윈도우여서...?' 라는 의심까지 도출됐다는 것이다.
여튼, 정확한 이유는 gradlw 파일의 EOL 때문이었다. 예전에 국비시절 자습 시간에 알게 된 사실이었는데, 윈도의 경우는 줄 바꿈이 캐리지 리턴 후에 줄 바꿈이 되고, 리눅스나 맥 OS의 경우는 바로 줄 바꿈 된다.(국비시절에 알게된 그 사실이 문제가 될줄은 상상도 못했다ㅎㅎ) 현재 내 컴퓨터는 윈도우이기 때문에 gradlew 파일이 CRLF(Carriage-Return, Line Feed)로 작성되어 있던 것! gradlew의 EOL을 LF로 변경해 주니 바로 잘 돌아갔다...ㅎ(Dockerfile은 잘못이 없었다... 항상 내잘못이지...ㅎ)
결론
앞서 Gradle에 대해서 공부하고 나니 Dockerfile을 이해하는 부분에 있어서는 크게 어려운 것은 없었다. 다만 이번 공부를 하며 Multi-stage build라는 이미지 빌드 프로세스 기술에 대해서 더 자세히 알게 되었고, 실제 실습에서 나는 오류들에 대한 파악에도 도움이 많이 되었다. 결론적으로는 빨리 윈도우에서 벗어나서 리눅스 환경에서 작업하고 싶다는 생각이 간절해졌다.ㅎㅎ
참고
- 영혼의 단짝 ChatGPT
- https://velog.io/@letskuku/%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-docker-compose-build%EC%97%90%EC%84%9C-%EB%B0%9C%EC%83%9D%ED%95%9C-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0
- https://bluese05.tistory.com/77
- https://velog.io/@layl__a/Docer-%EB%8F%84%EC%BB%A4-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88%EC%9D%98-%EC%A2%85%EB%A3%8C-%EC%BD%94%EB%93%9C-exit-code
'DevOps > Docker' 카테고리의 다른 글
배포 자동화에 들어가기 앞서 - CI/CD, docker hub, docker compose (0) | 2024.01.02 |
---|---|
배포 자동화에 들어가기 앞서 - Docker mysql container 생성, IDE에 docker DB container 연동 (0) | 2023.12.27 |
배포 자동화에 들어가기 앞서 - Docker 기초 (0) | 2023.12.27 |
댓글