เวลาเราเขียนโปรแกรมออกมาตัวนึง เราอาจจะมีการติดตั้งโปรแกรมที่เกี่ยวข้องต่าง ๆ หรืออาจจะเป็น Library ที่เราใช้งานต่าง ๆ แต่เมื่อเราเขียนเสร็จแล้ว เราต้องการเอาไปทำงานในเครื่องอื่น ๆ เช่น เครื่องที่ใช้งานจริง ๆ หรือที่เราเรียก Production ถ้าเราจะต้องมาติดตั้งทั้งหมดใหม่ ก็น่าจะไม่ใช่เรื่องที่ดีเท่าไหร่ ไหนจะเรื่องความเข้ากันได้ของ Software ต่าง ๆ หรือจะเป็นความผิดพลาดจากมนุษย์เอง เช่น การลืมลง Library หรือ Software ต่าง ๆ ก็ทำให้คนหัวหมุนกันมานัดต่อนัดแล้ว ทำให้วันนี้เราจะมาทำให้โปรแกรมของเรา สามารถนำไปใช้ต่อได้ง่ายขึ้นกันด้วย Docker
ปกติเราทำยังไงกัน ?
ก่อนเราจะไปดูว่า เราจะแก้ปัญหาด้วย Docker เราอยากจะมาบอกเล่าก่อนว่า ถ้าเกิดเราไม่ใช้ Docker เราเล่นมือเปล่าเลย เราจะต้องทำยังไง และ เราจะเจอปัญหาอะไรได้บ้างระหว่างนั้น
ใน Application ที่ง่ายที่สุด ส่วนประกอบของมันก้น่าจะเป็นพวก Source Code ที่เราเขียนเข้ามา ซึ่งใน Source Code เหล่านั้นเอง ก็อาจจะมีการ Import Dependencies อื่น ๆ เข้ามาร่วมด้วย ทำให้ตอนที่เราจะใช้งานตอนพัฒนา เราก็ต้องไป Download และติดตั้งผ่าน Package Manager ต่าง ๆ เช่น pip เป็นต้น
> pip install numpy pandas keras tensorflow tensorflow-gpu
ทำให้เมื่อเราจะต้องเอา Application ของเราไปทำงานที่อื่น เราก็ต้องทำการติดตั้งให้เหมือนกับเครื่องที่ใช้พัฒนาอีก เราก็อาจจะจด Dependencies ต่าง ๆ ใส่กระดาษ แล้วมาติดตั้งที่เครื่องที่จะทำงานก่อนที่เราจะสั่งให้ Application ของเราทำงาน แน่นอนว่า การทำแบบนี้ อย่างแรกเลยที่เจอได้คือ เราติดตั้งไม่ครบ พิมพ์ผิดบ้าง อะไรบ้างก็มึน ๆ กันไป อีกปัญหาที่ตามมา และเป็นเรื่องใหญ่ไม่แพ้กันเลยคือ Version ของ Dependencies ก็อาจจะไม่ตรงกับเครื่องที่เราใช้พัฒนา บางที Version เปลี่ยน บาง Function หรือ Module ก็อาจจะหายไป หรือเปลี่ยนที่อะไรก็ว่ากันไป ทำให้ Application ของเราทำงานไม่ได้นั่นเอง
> pip freeze >> requirements.txt
> pip install -r requirements.txt
จาก 2 ปัญหาที่ได้เล่าไป ทำให้เราเอาใหม่ ในบรรทัดแรก เราขอ Dependencies ทั้งหมดที่เราติดตั้งจาก pip ผ่านคำสั่ง pip freeze และให้มันเขียนไปที่ไฟล์ชื่อ requirements.txt จากนั้น เราก็ให้ pip มันไปอ่านไฟล์ที่พึ่งเขียนไป และให้ติดตั้งตามนั้นเลย ถ้าเราลองรันแล้วลองเปิดไฟล์ requirements.txt เราจะเจอแบบด้านล่างนี้
Flask==1.1.2
joblib==0.16.0
Keras==2.4.3
Keras-Preprocessing==1.1.2
scikit-learn==0.24.1
scipy==1.4.1
seaborn==0.10.1
เราจะเห็นได้ว่า มันจะยัดพวก Dependencies ที่เราติดตั้งทั้งหมดเข้ามาให้ พร้อมกับ Version ของแต่ละ Dependencies เลย ทำให้เมื่อเราสั่งให้ pip มาติดตั้งตามไฟล์นี้ มันก็จะเลือกใน Version ที่เราระบุไว้ในไฟล์ให้เลย ทำให้เรามั่นใจได้ว่า เราจะได้ Dependencies ครบ และ อยู่ใน Version เดียวกันกับที่เราใช้พัฒนาแน่นอน
แต่ปัญหามันก็ยังไม่หมด เพราะบาง Dependencies ก็ไม่ Support บาง Configuration ด้วย ตัวอย่างที่พึ่งเจอมาสด ๆ ร้อน ๆ เลยคือ Japronto ที่เราเจอปัญหาในการติดตั้ง เราลองเท่าไหร่ก็มีปัญหา ติดตั้งไม่ได้สักที พอมาดู Error Log ก็คือ มันมีปัญหากับ C Compiler ในเครื่องเรา ก็ถือว่าเป็นปัญหาที่เจอได้ นี่ละ คือสิ่งที่จะบอกว่า บาง Dependencies มันก็ทำตัวไม่น่ารักกับ Configuration บางอย่างได้เหมือนกัน เช่น OS หรือ C Compiler ต่าง ๆ ซึ่งต้องยอมรับว่า บางทีเราก็ไม่รู้เลยนะ จนมานั่งติดตั้งเองนี่แหละ ทำให้ Docker มันเข้ามาช่วยเราเรื่องนี้เยอะมาก เพราะมันเก็บเป็น Image ที่ Build ไว้ล่วงหน้า ทำให้เรามั่นใจได้ว่า ใน Container ที่เราทำงานจะเป็น Environment เดียวกันหมดได้แน่นอน การทำงานก็ เราก็ทำการ Copy Image ไปไว้ในเครื่องปลายทาง แล้วก็สั่งรันได้เลยตรง ๆ
Spoil เลยว่า ตอนที่ติดตั้ง Japronto ก็คือ เราจบที่การไปโหลด Docker Image เขามาใช้ก็รันได้เลย ฮ่า ๆ
Containerise Python Application ด้วย Docker
ก่อนที่เราจะเริ่มลองตัวอย่างกัน ให้ทุกคนเข้าไปติดตั้ง Docker กันก่อนที่จะทำด้วยที่ Docker Website
Project
└─── src
└─── server.py
ตัวอย่างของ Application ที่เราจะมาลองยัดใส่ Docker Container กันในวันนี้จะเป็น Application ง่าย ๆ อย่าง Hello World ใน Flask ก่อน เริ่มจากให้เราสร้าง Folder เปล่าเป็น Project ของเราขึ้นมา ในนั้นเราก็สร้าง Folder ชื่อ src และสร้างไฟล์ชื่อ server.py ขึ้นมาใน src ทำให้ Project Directory จะเป็นแบบด้านบน
> pip install flask
สำหรับผู้ที่ยังไม่เคยใช้ Flask มาก่อน ให้เราทำการติดตั้งผ่าน pip ได้ตรง ๆ เลย ในที่นี้อาจจะใช้เป็น Virtual Environment ก็ได้เหมือนกัน แต่ในบทความนี้เราจะให้ติดตั้งตรง ๆ ลงไปในเครื่องของเราเลยละกัน
from flask import Flask
server = Flask(__name__)
@server.route("/")
def index():
return "Hello World!"
if __name__ == "__main__":
server.run(host='0.0.0.0')
และใน server.pyเราจะมาใช้งาน Flask กัน เป็น Application ง่าย ๆ ที่ถ้าเราทำการรันแล้ว เมื่อเราเข้าไปในหน้าเว็บไซต์ เราจะได้ข้อความว่า Hello World ออกมา
> python src/server.py
ก่อนเราจะข้ามขั้นตอนนี้ไป ลองสั่งรัน server.py กันก่อนด้วยคำสั่งด้านบนใน Shell กันก่อน โดยที่ให้เราอยู่ใน Directory ของ Project เรา
> python src/server.py
* Serving Flask app "server" (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: off
* Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
ถ้าไม่มี Error อะไรก็แปลว่า Application ของเราก็พร้อมที่จะไปยัดใส่ Docker Container กันแล้ว
> pip freeze | grep 'Flask' >> requirements.txt
จากนั้นเราจะมาสร้าง File ที่กำหนด Dependencies ที่เราจะต้องใช้กัน จะใช้วิธีเดียวกับที่ได้เล่าไปก่อนหน้าแล้ว แต่ด้วยตอนที่เราติดตั้ง Dependencies เราอาจจะติดตั้งลงไปในเครื่องของเราโดยตรงเลย เราไม่ได้ใช้ Virtual Environment ถ้าเราเรียก pip freeze เราจะได้ Dependencies ตัวอื่นติดมาด้วย ทำให้ขนาดของ Application เราใหญ่เกินจำเป็น เราเลยใช้ Piping เข้าไปที่คำสั่ง grep เพื่อให้มันเอามาเฉพาะบรรทัดที่ประกอบด้วย Flask ซึ่งเป็น Dependencies ที่เราใช้ และให้มันเอาบรรทัดที่ได้ไปใส่ในไฟล์ requirements.txt ผ่าน Redirection Operator (ลองไปหาอ่านเพิ่มดูได้)
FROM python:3.9
WORKDIR /code
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY src/ .
CMD ["python", "./server.py"]
ขั้นตอนต่อไป จะเป็นการสร้าง Dockerfile เพื่อเป็นการระบุว่า เราจะให้ Container ของเรามีหน้าตาการทำงานอย่างไร เราลองมาดูทีละบรรทัดกัน
FROM python:3.9
บรรทัดแรก เป็นการไปเรียก Docker Image ที่มีอยู่แล้วมาเป็นพื้นก่อน ซึ่งในที่นี้เราเลือกใช้ Image ที่เป็น Python 3.9 มา ซึ่งเราก็สามารถไปเรียกจากที่อื่นที่เป็น Python ก็ได้เหมือนกัน หรือ เราจะไปเรียก Image พวกที่เป็น OS อย่าง Ubuntu เข้ามา และติดตั้ง Python เองก็ทำได้ แต่เพื่อความสะดวก เราก็ใช้ของที่เราติดตั้งมาสำเร็จแล้วจะง่ายกว่า
WORKDIR /code
คำสั่งด้านบนเปรียบเสมือนกับเราสั่ง cd ใน Shell เลย ก็คือเลือก Path ที่เราต้องการจะทำงานต่อไป
COPY requirements.txt .
สิ่งที่เราต้องการจะทำคือ การติดตั้ง Dependencies แต่ใน Image ที่เราต้องการจะสร้าง มันยังไม่มี File ที่ระบุ Dependencies เลย ทำให้ในขั้นตอนแรก เราจะต้องทำการ Copy requirements.txt จากเครื่องของเรา เข้าไปที่ Image ก่อน เราจะเห็นว่า หลังจาก COPY Argument ตัวแรก จะเป็น Path ไปที่ไฟล์ที่เราต้องการ Copy และอีกตัวจะเป็น Path ของ Image ในที่นี้เราใส่เป็นจุดเฉย ๆ ก็คือ Current Path หรือก็คือ*/code* ที่เราสั่งมันไว้ในบรรทัดก่อนหน้าแล้ว หลังจากจบบรรทัดนี้ไฟล์ requirements.txt ของเราก็จะเข้าไปอยู่ใน Image แล้ว
RUN pip install -r requirements.txt
หลังจากที่เราอัดไฟล์รายการ Dependencies เข้าไปแล้ว ก็สั่งให้มันไปติดตั้ง Dependencies จากรายการที่เราพึ่งยัดเข้าไปกัน ผ่านคำสั่ง RUN ซึ่งเทียบเท่ากับการสั่งรันคำสั่งใน Shell นั่นเอง
COPY src/ .
เราก็สั่งให้มัน Copy ตัว Source Code ที่เราเขียนไว้ เข้าไปที่ Image กัน ซึ่งก็จะเป็นไฟล์ที่อยู่ใน Folder src ทั้งหมด ให้ Copy ไปที่ /code ใน Image นั่นเอง
CMD ["python", "./server.py"]
สุดท้าย เราก็สั่งให้ Script ของเราทำงาน ผ่านการรันคำสั่ง python ./server.py แต่ใน Dockerfile เราจะใช้คำสั่งชื่อ CMD แล้วใส่โปรแกรมที่เราต้องการรัน พร้อมกับ Argument ต่าง ๆ ถ้าเราสังเกตุดี ๆ เราจะเห็นว่า CMD และ RUN ต่างเป็นคำสั่งที่สั่งให้ไปรันคำสั่งใน Shell เหมือนกันทั้งคู่ แต่จะต่างกันที่ว่า RUN จะทำงานแล้วเก็บผลการทำงานเอาไว้เป็น Cache แต่ CMD จะเป็นการรัน เมื่อเราสั่งให้ Container ทำงาน
> docker build -t my-first-python-app .
เมื่อเราเขียน Dockerfile เสร็จแล้ว ย้อนกลับมาที่ Shell ของเราและรันคำสั่งด้านบนเพื่อสั่งให้ Docker สร้าง Image จาก Dockerfile ที่เราพึ่งเขียนขึ้นมากัน โดยที่เราตั้งชื่อว่า my-first-python-app ซึ่งเราสามารถเปลี่ยนได้ตามใจชอบเลย และตัวสุดท้ายที่เป็นจุดเฉย ๆ เป็นการบอกตำแหน่งของ Dockerfile ก็คือบน Path ปัจจุบันที่เราสั่งรัน และไฟล์ชื่อ Dockerfile นั่นเอง
> docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
my-first-python-app latest 7b12bedf9710 2 mins ago 895MB
เพื่อความมั่นใจว่า Image ของเราถูกสร้างเรียบร้อยแล้ว ถ้าเราลองสั่ง docker images เพื่อขอดู Image ที่เรามีในเครื่อง เราจะเห็นว่าใน List จะมี Image ชื่อ my-first-python-app ที่เราพึ่งสร้างขึ้นมาด้วย
> docker run -d -p 5000:5000 --name my-app my-first-python-app
สุดท้าย เราก็ทำการสั่งสร้าง Container จาก Image ด้วยคำสั่ง run โดยที่เราเพิ่ม Argument หลาย ๆ ตัวเข้ามาคือ -d สำหรับ Detach หน้าต่างของ Shell ของ Container ออกไป ก็คือรันอยู่บน Background นั่นเอง
-p 5000:5000 เป็นการ Map Port ระหว่าง Port ในเครื่องเรา กับ Port ใน Container เข้าด้วยกัน ในที่นี้เราก็บอกว่า ให้ Port ในเครื่อง 5000 ไปเชื่อมต่อกับ Port 5000 ใน Container นั่นเอง ส่วน –name ก็ตรงตัวเลยก็คือชื่อของ Container และสุดท้ายเป็นชื่อ Image ที่เราสั่งสร้างขึ้นมาก่อนหน้า
เท่านี้ Application ที่เราเขียนขึ้นมาก็ทำงานเรียบร้อยแล้ว ในตัวอย่างนี้ เราเป็น Web Application ถ้าเราลองเข้าไปที่ localhost:5000 เราก็จะเจอกับ Hello World เหมือนตอนที่เราสั่งรันตรง ๆ ในเครื่องของเรานั่นเอง ก็เป็นอันเรียบร้อยแล้ว
เก็บ Image ที่ทำแล้วไปทำงานที่เครื่องอื่น
> docker save -o my-image.tar my-first-python-app
อย่างที่เราบอกว่า เราสามารถ Copy Image ไปทำงานในเครื่องอื่น ๆ ได้ เราจะใช้คำสั่ง save กัน โดยที่เราใส่ Path ที่เราต้องการให้ Image มัน Save ในที่นี้เราตั้งเป็น my-image และให้นามสกุลเป็น .tar ด้วยนะ และสุดท้ายเราก็ใส่ชื่อของ Image เข้าไปก็เรียบร้อยในฝั่งต้นทาง
> docker load -i my-image.tar
ที่เครื่องปลายทาง เราติดตั้ง Docker ลงไปเช่นกัน ไม่สำคัญว่าจะเป็น OS เดียวกัน หรือ Configuration เหมือนกัน แค่ลง Docker ได้พอ หลังจากนั้น เราก็ทำการโหลด Image เข้าไปผ่านคำสั่ง load ในนั้นเราใส่ Path ของ Image เราได้เลย สุดท้ายเราก็เอา Image ที่โหลดไปสร้างเป็น Container ได้เหมือนกับเครื่องต้นทางแล้ว
หรือจริง ๆ เราอาจจะไปใช้งานพวก Remote Repository ต่าง ๆ ก็ได้แต่เราจะไม่ได้ Cover ในบทความนี้กัน
สรุป
การที่เราเอา Application ของเราขึ้นมาอยู่บน Docker Container เข้ามาช่วยแก้ปัญหาเรื่องการ Reproduce Environment ต่าง ๆ ได้อย่างหมดจดเลย ทำให้เรามั่นใจว่า ไม่ว่าเราจะเอา Application ของเราไปรันที่ไหน มันก็ยังสามารถทำงานได้เหมือนกันหมด ตราบใดเราทำงานผ่าน Docker นั่นเอง เป็นเรื่องดี ทั้งสำหรับการทำงานร่วมกันในทีม จนไปถึงตอนที่เราจะ Deploy Application ของเรากัน สุดท้ายนาทีขายของแล้ว ถ้าอยากจะมาเรียนรู้การ Deploy Machine Learning Model และนำมาใช้งานบนเว็บของเรากัน เรามี Course มาฝากกันคือ Machine Learning Web with Scikit-learn, Flask, and Docker ที่จะพาไป Workshop กันตั้งแต่การสร้าง Model จนถึงการ Deploy Model กันไปเลย
