만일 필요한 서비스를 제공하는 이미지가 없다면 어떻게 해야 할까요? 이 경우에는 직접 이미지를 만들어야할 것입니다. 이번 장에서는 python flask
를 이용한 간단한 웹 애플리케이션을 만들어 보도록 하겠습니다.
먼저 우리는 어떤 것을 컨테이화하고 이미지를 통해서 어떤 어플리케이션을 만들거고 어떻게 애플리케이션을 만들어나갈지를 이해해보도록 하겠습니다.
아래와 같은 스텝을 중점으로 진행하겠습니다.
- OS - Ubuntu
- Update apt repo
- Install dependencies using apt
- Install python dependencies using pip
- Copy source code to /opt folder
- Run the web server using "flask" command
이미지를 만들기 위해서 먼저 우리는 Dockerfile이 필요합니다. 아래와 같은 docker file을 만들어 줍니다.
FROM ubuntu
RUN apt-get update
RUN apt-get install python
RUN apt-get -y install pip
RUN pip install flask
RUN pip install flask-mysql
COPY . /opt/source-code
ENTRYPOINT FLASK_APP=/opt/source-code/app.py flask run
이렇게 docker file을 만들어 준 후에는 빌드를 진행해야합니다.
linux> docker build Dockerfile -t parkchoongho/my-flask-app
docker build
명령어에 파일과 해당 이미지에 붙을 태그 이름을 같이 넘겨줍니다. 이렇게 하면 우리 시스템에 이미지가 만들어질 것입니다. 만약 도커 허브에 이를 올리고 싶다면 docker push
명령어를 입력해줍니다.
linux> docker push parkchoongho/my-flask-app
넘겨준 이름은 parkchoongho
계정 이름과 my-flask-app
이미지 이름으로 구성됩니다.
Dockerfile
은 특정 포맷으로 쓰여진 텍스트 파일입니다. 기본적으로 INSTRUCTION
과 ARGUMENT
로 구성됩니다.
INSTRUCTION ARGUMENT
FROM ubuntu
RUN apt-get update
RUN apt-get install python
RUN apt-get -y install pip
RUN pip install flask
RUN pip install flask-mysql
COPY . /opt/source-code
ENTRYPOINT FLASK_APP=/opt/source-code/app.py flask run
아까 살펴본 도커파일을 보면 왼쪽에 대문자로 인스트럭션이 작성되어 있는것을 볼 수 있습니다. FROM
, RUN
, COPY
, ENTRYPOINT
모두 인스트럭션입니다. 이미지를 만드는 동안 각각의 인스트럭션은 도커가 특정한 일을 하게끔 지시합니다.
오른쪽에 있는 것들은 모두 ARGUMENT
입니다. 첫번째 줄은 컨테이너가 어떤 OS 또는 어떤 이미지를 베이스로 하는지 나타냅니다. 모든 도커 이미지는 기본 OS 또는 다른 이미지를 기반으로 해야합니다. 도커 허브에서 릴리즈된 공식적인 운영체제들을 찾을 수 있습니다. 모든 도커 파일들이 RUN
인스트럭션부터 시작한다는 것을 기억하는 것은 중요합니다.
RUN
인스트럭션은 도커가 특정 명령어들을 base 이미지에서 실행하도록 합니다. 필요한 dependency들을 설치하고 난후, COPY
인스트럭션이 로컬 파일 시스템 현재 위치에 있는 모든 파일들을 도커 이미지 /opt/source-code
로 복사합니다. ENTRYPOINT
인스트럭션은 이미지가 컨테이너화 된 후 동작할 때 실행할 명령어를 특정합니다.
FROM ubuntu
RUN apt-get update && apt-get -y install python && apt-get -y install pip
RUN pip install flask flask-mysql
COPY . /opt/source-code
ENTRYPOINT FLASK\_APP=/opt/source-code/app.py flask run
도커가 이미지를 빌드할때 layered architecture
로 빌드합니다. 각 줄의 인스트럭션은 전 단계에 있었던 층으로부터 도커 이미지의 새로운 층을 생성합니다.
Layer 1. Base Ubuntu Layer
Layer 2. Changes in apt packages
Layer 3. Changes in pip packages
Layer 4. Source code
Layer 5. Update Entrypoint with "flask" command
각각의 층은 바로 전에 있던 층에서 부터의 변화만을 저장하기에 각 층마다의 사이즈를 확인할 수 있습니다.
Layer 1. Base Ubuntu Layer 120MB
Layer 2. Changes in apt packages 306MB
Layer 3. Changes in pip packages 6.3MB
Layer 4. Source code 229B
Layer 5. Update Entrypoint with "flask" command
docker history [Docker Image name or Id]
를 통해 위 정보에 대해서 얻을 수 있습니다.
linux> sudo docker history choonghopark/my-custom-app
IMAGE CREATED CREATED BY SIZE COMMENT
da05533686b3 3 minutes ago /bin/sh -c #(nop) ENTRYPOINT \["/bin/sh" "-c… 0B
dec5842ef794 3 minutes ago /bin/sh -c #(nop) COPY dir:19bebc10d64aceded… 203B
3e16b15b4cf3 3 minutes ago /bin/sh -c pip install flask flask-mysql 5.03MB
0734cf4e7797 3 minutes ago /bin/sh -c apt-get update && apt-get -y inst… 345MB
7e0aa2d69a15 5 weeks ago /bin/sh -c #(nop) CMD \["/bin/bash"\] 0B
<missing> 5 weeks ago /bin/sh -c mkdir -p /run/systemd && echo 'do… 7B
<missing> 5 weeks ago /bin/sh -c \[ -z "$(apt-get indextargets)" \] 0B
<missing> 5 weeks ago /bin/sh -c set -xe && echo '#!/bin/sh' > /… 811B
<missing> 5 weeks ago /bin/sh -c #(nop) ADD file:5c44a80f547b7d68b… 72.7MB
docker build
명령어를 통해 이미지를 생성하면 여러 단계를 거쳐서 이미지를 빌드하는 것을 확인할 수 있습니다.
Step 1/5 : FROM ubuntu
---> 7e0aa2d69a15
Step 2/5 : RUN apt-get update && apt-get -y install python && apt-get -y install pip
---> Running in 4147d1f1509b
Get:1 [http://security.ubuntu.com/ubuntu](http://security.ubuntu.com/ubuntu) focal-security InRelease \[114 kB\]
Get:2 [http://archive.ubuntu.com/ubuntu](http://archive.ubuntu.com/ubuntu) focal InRelease \[265 kB\]
Get:3 [http://security.ubuntu.com/ubuntu](http://security.ubuntu.com/ubuntu) focal-security/restricted amd64 Packages \[274 kB\]
...
각 단계마다 어떤 일이 발생하는지 우리는 확인할 수 있습니다. 모든 layer들은 캐싱됩니다. 따라서 layered architecture는 당신이 docker build를 실패하거나 특정 단계를 추가해도 그전에 수행된 단계들을 수행하지 않아도 되게끔 도움을 줍니다.
import os
from flask import Flask
app = Flask(__name__)
...
...
color = 'red'
@app.route("/")
def main():
print(color)
return render_template('hello.html', color=color)
@app.route('/how are you')
def hello():
return 'I am good, how about you?'
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080)
위 코드를 보면 color = 'red'
코드를 통해서 해당 html 파일의 배경을 빨간색으로 만들려고 함을 확인할 수 있습니다. 그런데 만약 이 색깔을 바꾸자 한다면 개발자가 해당 애플리케이션의 코드를 변경해야할 것입니다. 이를 방지하기 위해 해당 정보들을 애플리케이션 밖으로 꺼내 환경변수에 셋팅하는 방법을 사용할 수 있습니다.
import os
from flask import Flask
app = Flask(__name__)
...
...
color = os.environ.get('APP_COLOR')
@app.route("/")
def main():
print(color)
return render_template('hello.html', color=color)
@app.route('/how are you')
def hello():
return 'I am good, how about you?'
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080)
이렇게 color
변수에 환경변수 값을 불러오게끔 설정을 한뒤
linux> export APP_COLOR=blue; python app.py
이렇게 실행하면 코드를 변경하지 않고도 배경 색깔을 변경할 수 있습니다. 만약 이것을 도커화하고 싶다면 어떻게 해야할까요? -e
옵션을 사용하면 가능합니다. -e
옵션을 활용하면 컨테이너의 환경변수를 설정할 수 있습니다.
linux> docker run -e APP_COLOR=blue simple-webapp-color
동작하고 있는 컨테이너의 환경변수는 어떻게 알 수 있을까요? 앞서 배운 docker inspect
명령어로 알 수 있습니다.
linux> sudo docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
86fc9b15ba18 my-simple-webapp "/bin/sh -c 'FLASK\_A…" 44 minutes ago Exited (137) 41 minutes ago goofy\_turingcorretto-dev-1@corretto-dev-1 ~ 
❯❯❯ sudo docker inspect 86
...
...
"Config": {
"Hostname": "86fc9b15ba18",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": true,
"AttachStderr": true,
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
...
컨테이너에 동작하는 프로세스가 없을 경우 컨테이너가 자동으로 종료된다는 것은 앞서 설명드린바 있습니다. 컨테이너는 특정 프로세스를 동작하기 위해서 존재하는 것이라 봐도 무방한대요. 그것이 가상머신과 가장 구별되는 차이점이라 할 수 있습니다.
그렇다면 어떤 프로세스를 컨테이너에서 동작 시킬것인지 결정하는 것은 무엇일까요? Dockerfile에서 CMD
는 어떤 프로세스를 동작시킬것인지를 결정합니다. 만약 우리가 일반 우분투 이미지를 동작시킨다고 가정해봅시다. 해당 도커파일은
...
...
CMD ["bash"]
...
...
이렇게 되어 있기에 bash 프로그램을 동작 시킬것입니다. 하지만 도커가 터미널을 컨테이너와 기본적으로 연결시켜주지는 않기에 bash 프로그램은 터미널을 찾지 못하고 종료되기 때문에 프로그램이 종료됩니다. 이를 방지하기 위해서 기존의 명령어를 오버라이드 하고 실행하기 위해 명령어를 넘겨줍니다.
linux> docker run ubuntu [COMMAND]
linux> docker run ubuntu sleep 5
만약 이거를 고정적으로 바꾸고 싶을 떄는 어떻게 해야할까요? Dockerfile에 CMD sleep 5
를 추가해주면 됩니다.
FROM ubuntu
...
CMD sleep 5
...
CMD sleep 5
같이 CMD command param1
형태도 가능하지만 CMD ["sleep", "5"]
처럼 CMD ["command", "5"]
같은 형태도 가능합니다. 두번째 처럼 JSON array 형태로 줄때는 첫번째 인자는 반드시 실행 가능한 프로그램이어야합니다. 예를 들어, CMD ["sleep 5"]
처럼 작성할 수는 없습니다.
만약 프로그램이 sleep하는 초를 바꾸고 싶다면 어떻게 해야할까요? 우선
linux> docker run ubuntu-sleeper sleep 10
이런 식으로 새로운 명령어를 넘겨서 할 수 있지만 우리가 이미 Dockerfile에 sleep 5
명령어를 입력한 만큼 sleep
명령어를 2번 사용하는 것은 그만큼 효율적입니다. 우리는 아래와 같은 방식으로 10초로 늘리고 싶습니다.
linux> docker run ubuntu-sleeper 10
이런 경우에는 어떻게 해야 할까요? 이런 상황에서 ENTRYPOINT
가 활용됩니다. entry point 인스트럭션은 컨테이너가 시작할 때 어떤 프로그램이 동작할지를 알려주는 인스트럭션입니다.
FROM ubuntu
ENTRYPOINT ["sleep"]
이렇게 작성한 후에
linux> docker run ubuntu-sleeper 10
이렇게 하면 해당 컨테이너가 10초 동안 sleep하는 것을 확인할 수 있습니다. 이렇게 ENTRYPOINT에서는 명령어 파라미터가 뒤에 붙는 형태로 동작하는 것을 확인할 수 있습니다.
그런데 위 상황에서 아래와 같이 아무런 parameter 없이 컨테이너를 동작하면 sleep
명령어만 있기에 컨터이너가 동작하지 않는 에러를 맞이하게 됩니다. 만약 어떤 parameter도 특정되지 않았다고 했을 때 기본 값을 설정하고자 한다면 어떻게 해야할까요? 이 경우에는 CMD와 ENTRYPOINT를 동시에 활용하면 됩니다.
FROM ubuntu
ENTRYPOINT ["sleep"]
CMD ["5"]
이렇게 하면 궁극적으로 명령어는 sleep 5
가 됩니다.
linux> docker run ubuntu-sleeper
이렇게 하면 자동으로 sleep 5
를 실행합니다.
linux> docker run ubuntu-sleeper 10
이렇게 하면 parameter 10이 CMD를 덮어써 최종적으로 sleep 10
을 실행합니다. 그런데 만약 ENTRYPOINT를 덮어쓰고자 한다면 어떻게 해야 할까요?
linux> docker run --entrypoint sleep2.0 ubuntu-sleeper 10
--entrypoint
를 활용하게 되면 ENTRYPOINT를 덮어쓸 수 있게됩니다. 위 명령어는 최종적으로 컨테이너에서 sleep2.0 10
을 실행하게 됩니다.