왕초보 특별반 - 1. 나만의 서비스 컨테이너 이미지 제작

뚜비·2023년 8월 9일
1

2023년 오픈소스 컨트리뷰션 ARgo workflows 팀에서 활동한 기록입니다.
왕초보 특별반의 REPO
왕초보를 위한 도커 사용법
초보를 위한 도커 안내서 - 이미지 만들고 배포하기



0. 😎 왕초보 특별반?

왕초보 특별반은 Argo-worfklows 팀 내에서 컨테이너, 쿠버네티스, argo에 대해 잘 모르고 있는 뉴비들(It's me...)을 위한 특별반이다. 멘토님이 컨테이너, 쿠버네티스, argo에 관한 추가 과제를 내주시는데, 과제를 해결하면서 컨테이너, 쿠버네티스, argo에 대해 재미있고(?) 효율적으로 이해할 수 있을 것이라 기대한다.



1. 어떤 과제인가?

나만의 서비스 컨테이너 이미지 제작


ISSUE_TEMPLATE에 들어가보니 위과 같은 과제가 주어졌다!

웹서비스를 제작하고 컨테이너화 할 수 있는 도커 파일을 작성하는 과제인데, 다음과 같은 조건을 반드시 지켜야 한다.

🤔 과제 조건 요약
1. 디렉토리와 파일들은 먼저 나의 계정 디렉터리를 생성 후 그 아래에 작업하는데 Dockerfile이 반드시 위치해야 한다. (위의 멘토님의 예시 참고)
2. 빌드된 이미지는 반드시 8080 포트에서 동작해야 한다.
3. 웹서비스 개발언어 및 서비스 구조는 상관없으나 반드시 필수 api를 두 개 만들어야 한다.
4. Docker 이미지 최적화 하면 Good
5. 이슈 연결 및 이 밖의 참고사항은 첫 번째 지령 참고!!!



2. 나는 어떻게 수행했나?

이슈 연결 및 참고사항 check

첫 번째 지령에 더 자세히 나와 있으니 참고 하시길!

먼저 New Issue 버튼을 클릭한 후 위와 같이 Issue를 생성해주고..


해당 Repo를 fork 해주고 fork된 Repo를 clone해준다!


GitKraken > Preferences > GPG에 들어가보니 Signing Key가 잘 설정되어 있고

Github > settings > SSH and GPG Keys에 들어가보니 GPG Key도 잘 등록되어 있다!

cmd에 git config --list 라고 명령어를 입력하면 각종 설정을 확인할 수 있는데, user.signingkey 값은 GitKraken의 Singing Key에 등록된 Public Key의 키 지문(key fingerprint) 겂과 일치한다.

GPG를 이용한 서명 커밋은 문제 없을 듯하다!



웹서비스 제작

과제 조건 중에

UI, 디자인이 존재하지 않는 단순 api 서비스를 제작해도 상관 없습니다.

라는 조건이 있는데 우선 단순 api 서비스를 만들고 나중에 심심하면 UI와 디자인을 추가할지도..
나는 express 라는 node.js 웹 애플리케이션 프레임워크를 사용하여 웹서버를 만들어보겠다


우선 VSCode에 내 깃허브 계정 이름으로 된 폴더를 생성해준다.

해당 폴더 안에 Dockerfile을 생성해준다!
(VSCode extension의 docker extension을 설치해주면 자동 완성 기능을 함께 사용할 수 있으니 참고하시길)


✅ 과제 조건 [1. 디렉토리와 파일들은 먼저 나의 계정 디렉터리를 생성 후 그 아래에 작업하는데 Dockerfile이 반드시 위치] 완료



1. express 설치 및 환경 설정
나는 node와 npm은 이미 설치되어 있기 때문에 express만 설치해주면 된다.

우선 express 모듕을 설치해주기 전에 package를 생성해주자!

🤔 package.json이란?

  • npm에 의해 관리되는 파일이자, npm에 해당 패키지를 배포하고 다른 사람들이 관리하고 설치하기 쉽게 하기 위한 문서이다.
  • 현재 프로젝트에 관련한 여러 정보를 포함하고 있다.
  • 해당 프로젝트에 의존하고 있는 node.js의 패키지들은 package.json에서 확인할 수 있다.
    (출처 : 알고 쓰자 package.json)

npm init

생성해둔 폴더에 접근하여 위의 명령어를 입력하면

이렇게 package.json 파일이 생성된 것을 확인할 수 있다!


npm install express

이제 express 패키지를 설치해주면


package.jsondependencies에 express가 있는 것을 확인할 수 있다.
그리고 express 서버를 실행할 때 npm start로도 실행될 수 있도록

package.json의 scripts에 "start": "node app.js"을 추가해주자!


2. express 기반 웹서버 코드 작성

최상단 디렉토리에 app.js를 하나 생성해주고 다음과 같은 간단한 코드를 작성해준다.

const express = require('express'); // Load express modules 

const app = express(); // Return express application
const PORT = 3000; // set port 

/* api */
app.get('/api/v1/juijeong8324', function(req, res){
    res.status(200).json({
        "message" : "hello get api v1/juijeong8324"
    });
});

app.get('/healthcheck', function(req, res){
    res.send("hello get api /healthcheck");
});

/* starts a server and listens on port 3000 for connections */
app.listen(PORT, (err)=>{
    if(err) return console.log(err);
    console.log(`Server is on ${PORT}`);
});

3. 실행 및 test

npm start

VScode 터미널에서 위의 명령어로 실행하면
3000번 port에 서버가 실행되고 있음을 확인했고 주소창에 다음과 같이 접속해보면

http://localhost:3000/healthcheck

http://localhost:3000/api/v1/juijeong8324

위와 같이 잘 뜨는 것을 확인할 수 있다.



사실 너무 아무것도 없는 것 같아 home page를 만들어주고 버튼 클릭시 해당 api를 호출처리까지 추가해주었다^^

자세한 코드는 해당 레포에서 확인해보시길..^^

✅ 과제 조건 [3. 웹서비스 개발언어 및 서비스 구조는 상관없으나 반드시 필수 api를 두 개 만들어야 한다.] 완료



Docker 명령어 분석

docker build <옵션> <Dockerfile 경로>
docker build -t [이미지명] .
  • 이미지를 Build(생성)하는 명령어
  • 현재 디렉토리(.)에 있는 Dockerfile을 이용해서 이미지를 빌드한다.
  • -t 옵션 : 이미지 이름과 태그를 설정

docker run <옵션> <Image이름> <command> <arg..>
docker run -d -p [포워딩 포트번호]:8080 [이미지명]
  • 이미지를 컨테이너(이미지를 실행한 상태)로 실행하는 명령어
  • -d 옵션 : 컨테이너를 백그라운드로 실행
  • -p [포워딩 포트번호]:[8080]
    : 호스트의 포트(포워딩 포트)와 컨테이너의 8080포트를 연결
    : 호스트의 포트에서 run하고 컨테이너 포트로 접속해라 → ✏️ http://<호스트IP>:8080에 접속하면 컨테이너의 포트로 접속할 수 있다.

✅ 결론
doccker build → Dockerfile을 통해 해당 이미지를 빌드시켜 웹서비스가 실행될 수 있는 환경을 setting한다.
docker run → 이미지를 컨테이너로 실행시켜(마치 우리가 로컬에서 node app.js 하는 것처럼) 컨테이너에 접속하면 웹서비스를 이용할 수 있어야 한다.



Dockerfile 생성

Dockerfile은 무엇인가?

  • Docker 이미지 설정 파일이자 일종의 배치 파일, Dockerfile에 설정된 내용대로 이미지를 생성한다.
  • 특정 이미지를 기준으로 새로운 이미지 구성에 필요한 명령어들을 저장해 놓은 파일
  • 자체 DSL(Domain Specific Langudage)을 이용하여 이미지 생성과정을 명시

Dockerfile을 직접 생성해보자

Dockerfile reference
가장 빨리 만나는 Docker
초보를 위한 도커 안내서 - 이미지 만들고 배포하기



먼저 Dockerfile을 만들기전에 우리가 로컬에서 웹애플리케이션을 실행하기 위해 무엇을 했는지 생각해보자.

  1. node.js와 npm 로컬 환경 구축 (글에서는 생략, windows 기준 공식 웹 사이트에서 installer를 이용하여 설치)
  2. 각종 node package(express) 설치
  3. 소스 코드 작성
  4. npm start로 실행

즉 위의 과정을 Dockerfile이 대신 해줄 수 있게 하면 된다! 대신 3번의 소스 코드는 우리가 작성한 소스를 사용해야하니 복붙을 해야한다!


우선 멘토님이 작성한 예시를 확인해보자

# 1. 이미지 제작 전
FROM node:latest
MAINTAINER GeunSam2 <email>

# 2. 이미지 제작 중
RUN mkdir -p /app
WORKDIR /app
ADD . /app
RUN npm install

# 3. 이미지 제작 후 
ENV NODE_ENV development
EXPOSE 3000 80
CMD ["npm", "start"]

자세한 dockerfile의 DSL은 여기를 참고하시길!

  • 이미지 제작 전
    FROM node:latest node라는 이미지로부터 새로운 이미지를 생성할 것임을 지정
    MAINTAINER GeunSam2 <email> Dockerfile을 생성/관리하는 사람의 정보를 입력해준다.

  • 이미지 제작 중
    RUN mkdir -p /app /app 디렉토리를 생성, 이때 상위 디렉토리가 함께 생성(-p옵션)
    WORKDIR /app /app 디렉토리를 Workdir로 설정, 이후의 명령어 부터 /app 디렉토리에서 실행됨.
    ADD . /app 생성될 이미지의 /app 디렉토리에 현재 디렉토리(Dockerfile이 있는 경로)의 모든 파일을 복사.
    RUN npm install npm install로 npm package.json에 있는 모든 패키지 설치

  • 이미지 제작 후
    ENV NODE_ENV development 컨테이너에서 사용할 환경변수 설정 즉, $NODE_ENV = development가 됨
    EXPOSE 3000 80 컨테이너 포트를 3000번과 80번으로 지정
    CMD ["npm", "start"] 컨테이너 실행 이후 npm start 명령어 실행



멘토님의 예시 Dockerfile을 기반으로(사실 그냥 복붙이나 다름 없는..) Dockerfile을 작성해보자.

FROM node:latest -> 버전을 직접 적어주는게 좋다. 
MAINTAINER juijeong8324 juijeong8324@gmail.com

RUN mkdir -p /app -> WROKDIR로 바로 /app 디렉 생성
WORKDIR /app
ADD . /app -> ADD보다 COPY추천
RUN npm install

EXPOSE 8080
CMD ["npm", "start"]

Dockerfile로 이미지 빌드 시 전체적인 흐름은 다음과 같다.

  1. FROM으로 node.js와 npm 로컬 환경 구축
  2. ADD로 소스 코드 복사
  3. RUN npm install로 node package 설치(나의 소스 코드에 있는 패키지들을 설치해주는 작업)
  4. 도커 컨테이너 8080포트 설정 ✅ 과제 조건 [2. 빌드된 이미지는 반드시 8080 포트에서 동작해야 한다.] 완료
  5. npm start로 웹애플리케이션 실행

위에서 우리가 로컬에서 직접 웹애플리케이션을 실행하기 위해 setting한 과정과 비슷함을 알 수 있다.



잘 실행되는지 test해보자!

docker build -t [이미지명] .

먼저 내 폴더에 들어간 후 위와 같이 명령어를 입력해주었다.


자세히 보면 Dockerfile에 정의한 내용들이 실행되고 있다!(FROM node:latest도 있는데 어디로 갔는지 사라졌다!)


VsCode extension에서 docker 탭을 클릭하면 test라는 이름의 image가 잘 생성되었음을 확인할 수 잇다.


docker run -d -p 80:8080 test

로 실행해서 컨테이너로 올리면!



문제발생.. 웹 애플리케이션이 실행이 안 됨?


뭐야 저 로그가 뜨면 실행이 잘 된건데..?

아무것도 뜨지 않는다..


✅ 해결방법
잠깐!! 다들 기억하는가... 우리가 웹애플리케이션 서버 port를 3000번으로 지정했다는 것을..?
즉 우리의 서버는 3000번 포트를 통해 요청을 받고 응답을 해준다는 것인데..
이 서버가 컨테이너에서 실행되면 당연히 3000번 port를 통해 요청을 받고 응답을 해줘야 하는 것이 아닌가!!(아뿔싸..)
그런데 우리는 컨테이너 port를 8080으로 설정해뒀는데.. 당연히 요청이 안 들어오지!!!

나는 지금까지 착각하고 있었다.
1. 우선 컨테이너의 port와 서버의 port는 별개의 것이라 생각했고
2. 호스트 == 이미지를 만든 사람이라 생각하고 다른 컴퓨터의 컨테이너에서 실행되는 것이 호스트의 환경에서 실행되는 것을 가져와 연동한 것이라 착각한 것이다!!
컨테이너는 결국 가상환경이고 호스트는 그 가상환경을 사용하는 사람인 것인데!!! 나 뭐야?!!?

결국.. app.js에서 port를 8080으로 바꿔주니 아주... 잘 된다..^^

localhost 뒤에 80은 생략된 것! 아주 잘 작동함을 알 수 있다...

드디어 8080 port에서 동작해야 한다는 조건이 만족되었다.



Docker image 최적화

  1. .dockerignore에 node_modules 추가
  2. npm install 시 --production 옵션 추가
  3. 불필요한 레이어의 수를 줄이기
    RUN, COPY, ADD 명령문이 실행되면서 생성!
    한번 빌드한 이미지를 다시 빌드하면 굉장히 빠르게 완료!
    도커는 빌드할 때 Dockerfile의 명령어가 수정되었거나 추가하는 파일 변경 되었을 때 캐시가 깨지고 그 이후 작업은 새로 이미지리를 만들게 된다.
FROM node:latest
MAINTAINER juijeong8324 juijeong8324@gmail.com

RUN mkdir -p /app
WORKDIR /app
ADD package*.json /app
RUN npm install --production
ADD . /app


EXPOSE 8080
CMD ["npm", "start"]

위의 코드처럼 먼저 package.json을 먼저 복사하고 패키지를 설치한 후 소스를 전체 복사했다!
(즉, 소스파일이 변경되면(ADD) 캐시가 깨져서 이미지 생성 시 앞서 install 했던 것을 또 install 해야한다는 것!)
거의 3초만에 이미지를 빌드한다. (옆에 시간을 확인해보면 상당히 빠르다는 것을 확인할 수 있음)

->

package를 먼저 copy하는 것은 고급기술...
image로 빌드 시 각 명령어가 실행될 때마다 이미지 레이어로 저장되고 이를 다음 빌드 시 캐시로 사용된다.
만약 소스코드 혹은 Dockerfile이 변경되어 다시 빌드하게 되면 캐시가 깨지고 아예 새로운 이미지를 생성해야 하는데 이때, 패키지 설치시 시간이 매우 오래 걸리기 때문에 캐싱을 최대한 활용해줘야 한다.


빌드할 때 소스코드를 먼저 copy하게 되면 이전의 캐시(패키지 설치해둔 이미지)가 아예 사라지기 때문에 이미지 생성 시 캐시를 활용하지 못하고 패키지를 추가 install 해줘야 하는 상황이 발생(알다시피 패키지 설치는 꽤나 오랜 시간이 걸린다.)

이는 만약 내가 아주 사소한 소스 코드를 수정해줬는데 캐시를 통해 더 빠르게 빌드 할 수 있는 부분을 패키지부터 다시 오래 설치할 수도 있는 문제가....

근데 내가 작성한 최적화된 코드는 해당 이미지 빌드 시 package 설치 부분은 변경된 소스 코드를 복사하지 않는데, 이는 캐시가 깨지지 않아 레이어를 이미지에 가져다 써서 패키지 추가 설치 없이 바로 사용할 수 있다!! 그래서 더 빠른 거지 켈케렠레!!!



과연 결과는??!

DCO에 따른 커밋 및 PR을 날렸다.. 과연 결과는?!?!?

왕초보 기초반 과제 첫 번째 PR


크으으으... DCO 봇도 만족하고 Github actions도 한 번에 만족..


짜잔 보이는가 완벽한 통과...? 과제 성공!!!!



멘토님의 코드 리뷰 및 수정

추가 최적화를 요구하셨다!! 나중에 추가 정리



3. 나의 궁금증

1. docker image에서 Base Image는 무엇일까.. docker Base image는 운영체제인 것인가..! 그럼 node는 또 뭐야..?

걍 기본 설정 환경이라 생각하자 나중에 정리


2. 포워딩 port에서 실행하고 container의 port에 접속한다는 것은 무슨 소리?!

완벽이해 나중에 정리


3. 자바에서는 패키지 없이 실행파일만 image에 올려도 된다!

병선님은 개인적으로 하시는 자바 프로젝트를 Dockerfile로 올리셨는데..!!!

병선님의 PR << 여기로!!

FROM azul/zulu-openjdk-alpine:17-latest AS build

LABEL AUTHOR="qudtjs0753 <qudtjs3636@gmail.com>"

# 컨테이너 상에서의 작업 디렉토리로 전환, 로컬에서 실행되는 부분으로 이미지 생성 시 반영 안 됨 
WORKDIR /docker_api_test

COPY ./docker_api_test .

RUN ./gradlew build

# 실제 이미지에 적용되는 코드, 빌드된 결과물을 iamge에 담아서 실행
FROM azul/zulu-openjdk-alpine:17-jre-headless-latest as production

WORKDIR /api

COPY --from=build /docker_api_test/build/libs/docker_api_test-0.0.1-SNAPSHOT.jar /api

CMD ["java", "-jar", "docker_api_test-0.0.1-SNAPSHOT.jar"]

병선님이 올리신 방법은 다음과 같다.
1. 로컬에서 빌드하여 실행파일을 생성
2. 해당 실행파일을 imag에 포함(마지막 FROM 구문 부분)


근데 js는 이렇게 실행파일을 올릴 수 없다.. 패키지 종속성 문제 때문에!! 왜그럴까!?
컴파일 언어와 스크립트 언어의 차이 때문
자바는 컴파일 언어이기 때문에
컴파일 언어는 여러 종속적인 dependencies를 한번에 실행파일에 저장시켜주는데... 스크립트 언어는 실행될 때마다 라이브러리를 가져오는 특성이 있기 때문이다!!




4. 캐싱이 그 메모리의 캐시인가?

도커파일의 각 명령어 한 줄이 레이어가 됨..

최초 빌드 시는 오래 걸림
두번 빌드 시 빨라지는 이유가 캐싱 덕분인데
로컬에 저장된 레이어들을 명령어 수행하지 않고 링크해서 생성했더 그 이미지를 가져다 쓴다는 의미!!
그 레이어들은 tar파일에 다 저장되어 있단. 그 레이어들은 docker/overlay에 저장되어 있고
docker/overlay2(하드 디스크 안에 존재) 폴더 자체가 캐시
캐시를 이용해서 가져다 쓰는 방식이 그 overlayFS 시스템이기때문에 !!!

즉 이미지는 레이어를 쌓는 방식인게 이해가 가는가!!!

그럼 이미지를 삭제하면 캐시에 있는 이미지도 다 삭제되는 가? 그건 아님!!! 용량이 부족해지지 않으면 어느 정도 남겨놓음!!!

profile
SW Engineer 꿈나무 / 자의식이 있는 컴퓨터

2개의 댓글

comment-user-thumbnail
2023년 8월 9일

좋은 글이네요. 공유해주셔서 감사합니다.

1개의 답글