Docker容器化部署Python應用

j_hao104發表於2019-06-28

Docker容器化部署Python應用

1. 簡介

Docker是目前主流IT公司廣泛接受和使用的,用於構建、管理和保護它們應用程式的工具。

容器,例如Docker允許開發人員在單個作業系統上隔離和執行多個應用程式,而不是為伺服器上的每個應用程式專用一個虛擬機器。使用容器更輕量級,可以降低成本、更好地使用資源和發揮更高的效能。

本文將使用Flask開發一個簡單的Python web應用程式,併為“容器化”做好準備。然後建立一個Docker映像,並將其部署到測試和生產環境中。

注意: 請確保機器上已安裝Docker,如果沒有請參考Docker官方安裝教程

2. Docker介紹

Docker是一種工具,它使開發人員能夠交付他們的應用程式(以及庫或其他依賴項),確保他們可以使用正確的配置執行,而不受部署環境影響。

這是通過將應用程式隔離在單獨的容器中來實現的,這些應用程式雖然被容器分隔開,但是卻可以共享作業系統和其他資源。

Docker包含兩部分:

  • Docker Engine — 應用打包工具,用於封裝應用程式。

  • Docker Hub — 用於管理雲上容器應用程式的工具。

3.為何選擇容器

瞭解容器的重要性和實用性非常重要,雖然它和直接將應用部署到伺服器沒有多大區別,但是當涉及到比較複雜的且相當吃資源的應用,尤其是多個應用部署在同一臺伺服器,或是同一應用要部署到多臺伺服器時。容器就變得非常有用。

在容器之前,這是通過 VMWareHypervisor等虛擬機器解決的,但是它們在效率、速度和可移植性方面已被證明並不是最佳選擇。

Docker容器是虛擬機器的輕量級的替代品-與VM不同,我們不需要為它預先分配RAM、CPU或其他資源,也不需要為每個應用程式啟動一個VM,僅僅只需要一個作業系統即可。

使用容器開發人員就不需要為不同環境制定特殊版本,這樣可以專注於應用程式的核心業務邏輯。

4.建立Python應用

Flask是Python的一個輕量級Web應用框架,簡單易用,可以很快速地建立web應用。我們用它來建立此demo應用。

如果還沒有安裝Flask模組,可以使用下面命令安裝:

$ pip install flask

安裝成功後,新建一個應用目錄,命名為FlaskDemo。並在該目錄下建立應用程式碼檔案app.py

app.py中,首先引入Flask模組,然後建立一個web應用:

from flask import Flask

app = Flask(__name__)

然後定義路由/和其對應的請求處理程式:

@app.route("/")
def index():  
  return """
  <h1>Python Flask in Docker!</h1>
  <p>A sample web-app for running Flask inside Docker.</p>
  """

最後,新增執行主程式並啟動該指令碼:

if __name__ == "__main__":  
    app.run(debug=True, host='0.0.0.0')
$ python app.py

然後在瀏覽器中訪問http://localhost:5000/,可以看到Dockerzing Python app using Flask這樣的頁面。

Docker容器化部署Python應用

5.Dokcer打包應用

要在Docker上執行應用程式,首先必須構建一個容器,而且必須包含使用的所有依賴項——在我們的例子中只有Flask。因此,新建一個包含所有依賴包的 requirements.txt 檔案,然後建立一個Dockerfile,該檔案用來描述構建映像過程。

此外,當啟動容器時還需要放開應用程式的HTTP埠。

準備工作

requirements.txt 檔案非常簡單,只需要填入專案的依賴包和其對應版本即可:

Flask==1.0.2 

接下來,需要將應用程式執行所需的所有Python檔案都放在頂層資料夾中,例如,名為app的目錄。

同時建議將主入口程式命名為 app.py ,將指令碼中建立的Flask物件命名為 app 是一種通常的做法,這樣也可以簡化部署。

FlaskApp  
    ├── requirements.txt
    ├── Dockerfile
    └── app
        └── app.py
        └── <other .py files>

建立Dockerfile

Dockerfile本質上是一個文字檔案,其中明確定義瞭如何為我們的專案構建Docker映象。

接下來建立一個基於Ubuntu 16.04 和 Python 3.X的Dokcer映象:

FROM ubuntu:16.04

MAINTAINER jhao104 "j_hao104@163.com"

RUN apt-get update -y && \  
    apt-get install -y python3-pip python3-dev

COPY ./requirements.txt /requirements.txt

WORKDIR /

RUN pip3 install -r requirements.txt

COPY . /

ENTRYPOINT [ "python3" ]

CMD [ "app/app.py" ] 

Dockerfile的基本指令有十三個,上面用到了部分;

  • FROM - 所有Dockerfile的第一個指令都必須是 FROM ,用於指定一個構建映象的基礎源映象,如果本地沒有就會從公共庫中拉取,沒有指定映象的標籤會使用預設的latest標籤,如果需要在一個Dockerfile中構建多個映象,可以使用多次。

  • MAINTAINER - 描述映象的建立者,名稱和郵箱。

  • RUN - RUN命令是一個常用的命令,執行完成之後會成為一個新的映象,通常用於執行安裝任務從而向映像中新增額外的內容。在這裡,我們需更新包,安裝 python3pip 。在第二個 RUN 命令中使用 pip 來安裝 requirements.txt 檔案中的所有包。

  • COPY - 複製本機檔案或目錄,新增到指定的容器目錄, 本例中將 requirements.txt 複製到映象中。

  • WORKDIR - 為RUN、CMD、ENTRYPOINT指令配置工作目錄。可以使用多個WORKDIR指令,後續引數如果是相對路徑,則會基於之前命令指定的路徑。

  • ENTRYPOINT - 在啟動容器的時候提供一個預設的命令項。

  • RUN - 執行 app 目錄中的 app.py

Docker映象構建原理

Docker映象是使用 Docker build 命令構建的。在構建映象時,Docker建立了所謂的“層(layers)”。每一層都記錄了Dockerfile中的命令所導致的更改,以及執行命令後映象的狀態。

Docker在內部快取這些層,這樣在重新構建映象時只需要重新建立已更改的層。例如,這裡使用了 ubuntu:16.04 的基礎映象,相同容器的所有後續構建都可以重用它,因為它不會改變。但是,因為專案修改,在下次重新構建過程中 app 目錄的內容可能會有所不同,因此只會重新構建這一層。

需要注意的是,每當重新構建某一層時,Dockerfile 中緊隨其後的所有層也都需要重新構建。例如,我們首先複製 requirements.txt 檔案,然後再複製應用程式的其餘部分。這樣之前安裝的依賴項只要沒有新的依賴關係,即使應用程式中的其他檔案發生了更改,也不需要重新構建這一層。這一點在建立 Dockerfiles 時一定要注意。

因此,通過將 pip 安裝與應用程式其餘部分的部署分離,可以優化容器的構建過程。

構建Docker映象

現在 Dockerfile 已經準備好了,而且也瞭解了Docker的構建過程,接下來為我們的應用程式建立Docker映像:

docker build -t docker-flask:0.1 .

除錯模式執行

根據前面講到的容器化的優點,開發的應用程式通過容器部署,這從一開始就確保了應用程式構建的環境是乾淨的,從而消除了交付過程中的意外情況。

但是呢,在開發應用程式的過程中,更重要的是要快速重新構建和測試,以檢查驗證過程中的每個中間步驟。為此,web應用程式的開發人員需要依賴於Flask等框架提供的自動重啟功能(Debug模式下,修改程式碼自動重啟)。而這一功能也可以在容器中使用。

為了啟用自動重啟,在啟動Docker容器時將主機中的開發目錄對映到容器中的app目錄。這樣Flask就可以監聽主機中的檔案變化(通過對映)來發現程式碼更改,並在檢測到更改時自動重啟應用程式。

此外,還需要將應用程式的埠從容器轉發到主機。這是為了能夠讓主機上的瀏覽器訪問應用程式。

因此,啟動Dokcer容器時需要使用 volume-mappingport-forwarding 選項:

docker run --name flask_app -v $PWD/app:/app -p 5000:5000 docker-flask:0.1

改命令將會執行以下操作:

  • 基於之前構建的 docker-flask 映象啟動一個容器;

  • 這個容器的名稱被設定為 flask_app 。如果沒有 ——name 選項,Docker將為容器生成一個名稱。顯式指定名稱可以幫助我們定位容器(用來停止等操作);

  • -v 選項將主機的app目錄掛載到容器;

  • -p 選項將容器的埠對映到主機。

現在可以通過http://localhost:5000 或者 http://0.0.0.0:5000/ 訪問到應用:

Docker容器化部署Python應用

如果我們在容器執行的時候,修改應用程式程式碼,Flask會檢測到更改並重新啟動應用程式。

Docker容器化部署Python應用

要停止容器的話,可以使用 Ctrl + C, 並執行 docker rm flask_app 移除容器。

生產模式執行

雖然直接使用Flask裸跑執行應用程式對於開發來說已經足夠好了,但是我們需要在生產中使用更健壯的部署方法。

目前主流的部署方案是 nginx + uwsgi,下面我們將介紹如何為生產環境部署web應用程式。Nginx是一個開源web伺服器,uWSGI是一個快速、自我修復、開發人員和系統管理員友好的伺服器。

首先,我們建立一個入口指令碼,用來控制以開發模式還是生產模式啟動我們的應用程式,這兩者區別是選擇直接執行python還是nginx模式。

然後再寫一個簡單shell啟動指令碼 entry-point.sh:

#!/bin/bash

if [ ! -f /debug0 ]; then  
  touch /debug0

  while getopts 'hd:' flag; do
    case "${flag}" in
      h)
        echo "options:"
        echo "-h        show brief help"
        echo "-d        debug mode, no nginx or uwsgi, direct start with 'python3 app/app.py'"
        exit 0
        ;;
      d)
        touch /debug1
        ;;
      *)
        break
        ;;
    esac
  done
fi

if [ -e /debug1 ]; then  
  echo "Running app in debug mode!"
  python3 app/app.py
else  
  echo "Running app in production mode!"
  nginx && uwsgi --ini /app.ini
fi  

然後建立uWSGI配置檔案 app.ini:

[uwsgi]
plugins = /usr/lib/uwsgi/plugins/python3  
chdir = /app  
module = app:app  
uid = nginx  
gid = nginx  
socket = /run/uwsgiApp.sock  
pidfile = /run/.pid  
processes = 4  
threads = 2 

和nginx配置檔案 nginx.conf:

user nginx;
worker_processes  4;
pid /run/nginx.pid;

events {
    worker_connections  20000;
}

http {
    include    mime.types;
    sendfile on;
    keepalive_timeout  65;
    gzip off;

    server {
        listen 80;
        access_log off;
        error_log off;

        location / { try_files $uri @flaskApp; }
        location @flaskApp {
            include uwsgi_params;
            uwsgi_pass unix:/run/uwsgiApp.sock;
        }
    }
}

最後,修改DockerfilenginxuWSGI安裝到映象,將配置檔案複製到映象中,並設定執行nginx所需的使用者許可權:

FROM ubuntu:16.04

MAINTAINER jhao104 "j_hao104@163.com"

RUN apt-get update -y && \
    apt-get install -y python3-pip python3-dev && \
    apt-get install -y nginx uwsgi uwsgi-plugin-python3

COPY ./requirements.txt /requirements.txt
COPY ./nginx.conf /etc/nginx/nginx.conf

WORKDIR /

RUN pip3 install -r requirements.txt

COPY . /

RUN adduser --disabled-password --gecos '' nginx\
  && chown -R nginx:nginx /app \
  && chmod 777 /run/ -R \
  && chmod 777 /root/ -R

ENTRYPOINT [ "/bin/bash", "/entry-point.sh"]

然後重新打包映象:

docker build -t docker-flask:0.1 .

然後使用nginx啟動應用程式:

docker run -d --name flaskapp --restart=always -p 8091:80 docker-flask:0.1

該映象包含python、ngix、uwsgi完整環境,只需要在部署時指定埠對映便可以自動部署應用。要停止並刪除此容器,請執行下面命令:

docker stop flaskapp && docker rm flaskapp

此外,如果我們仍然需要上面除錯功能或修改部分程式碼,也可以像上面一樣以除錯模式執行容器:

docker run -it --name flaskapp -p 5000:5000 -v $PWD/app:/app docker-flask:0.1 -d debug

6.管理外部依賴

如果將應用程式作為容器交付時,需要記住的一個關鍵事項是,開發人員管理依賴項的責任增加了。除了識別和指定正確的依賴項和版本之外,還需要負責在容器環境中安裝和設定這些依賴項。

在Python專案中管理安裝依賴比較容易,可以使用requirements.txt指定依賴項和對應版本,然後通過 pip 安裝。

需要重申的是是,無論何時修改 requirements.txt 檔案,都需要重新構建Docker映象。

啟動時安裝依賴項

可能在某次版本更新時需要安裝額外的依賴項。比如,在開發過程中使用了一個新的包。如果不希望每次都重新構建Docker映象,或者希望在啟動時使用最新的可用版本。可以通過修改啟動程式在應用程式啟動時執行安裝程式來實現這一點。

同樣,我們也可以安裝額外的系統級包依賴項。修改 entry-point.sh:

#!/bin/bash

if [ ! -f debug0 ]; then
  touch debug0
  
  if [ -e requirements_os.txt ]; then
    apt-get install -y $(cat requirements_os.txt)
    
   fi
   if [-e requirements.txt ]; then
    pip3 install -r requirements.txt
   fi

  while getopts 'hd:' flag; do
    case "${flag}" in
      h)
        echo "options:"
        echo "-h        show brief help"
        echo "-d        debug mode, no nginx or uwsgi, direct start with 'python3 app/app.py'"
        exit 0
        ;;
      d)
        touch debug1
        ;;
      *)
        break
        ;;
    esac
  done
fi

if [ -e debug1 ]; then
  echo "Running app in debug mode!"
  python3 app/app.py
else
  echo "Running app in production mode!"
  nginx && uwsgi --ini /app.ini
fi

這樣我們可以在 requirements_os.txt 中指定將要安裝的系統軟體包名稱,這些包名以空格分隔放在同一行。他們將和 requirements.txt 中的Python依賴庫一樣在應用程式啟動之前安裝。

儘管這樣對應用的迭代開發期間提供了便利,但是出於幾個原因,在啟動時安裝依賴項不是一個好的實踐:

  • 它破壞了容器化的目標之一,即修復和測試由於部署環境的變化而不會改變的依賴關係;

  • 增加了應用程式啟動的額外開銷,這將增加容器的啟動時間;

  • 每次啟動應用程式時需要安裝依賴項,這樣對網路資源有要求。

相關文章