Retrofitting Apps for Cloud Storage with Zero Code Changes

Michael Lynch

零程式碼修改,為應用程式加裝雲端儲存

我最近在一台伺服器上安裝了一款媒體分享應用程式。安裝過程很簡單,但它卻為長期維護埋下了一個棘手的陷阱。

每當使用者上傳檔案時,網頁應用程式就會將檔案儲存到本機檔案系統中。如果我哪天重灌或重建伺服器,就必須手動備份並還原所有檔案。更好的架構是讓應用程式將檔案寫入獨立的儲存伺服器,但我不想為此花上數個月重寫應用程式。

初始架構與理想架構對比

透過 Docker、Google Cloud Storage 以及 gcsfuse 公用程式,我在完全沒有修改應用程式任何一行程式碼的情況下,實現了這種分離。

在本教學中,我將示範如何在盡量不更動原始碼的前提下,為舊有應用程式加裝 Google Cloud Storage 支援。

名詞定義

如果你對 Google Cloud Platform 不熟悉,以下是本教學會用到的幾個縮寫:

縮寫全名說明類似於
GCSGoogle Cloud StorageGoogle 的雲端儲存服務Amazon S3
GCEGoogle Compute EngineGoogle 的隨需虛擬機器服務Amazon EC2
GCRGoogle Container RegistryGoogle 的 Docker 映像檔代管服務Docker Hub
GCPGoogle Cloud PlatformGoogle 的雲端運算平台(GCS、GCE 和 GCR 皆為 GCP 的一部分)Amazon Web Services 或 Microsoft Azure

Docker 標誌

Docker 讓開發者能為應用程式打造可在任何地方執行的自給自足環境:

  • Docker image 是執行應用程式所需的所有檔案集合,包含作業系統與所有第三方相依套件。
  • Docker container 則是 Docker image 實際執行時的動態環境。

在本教學中,你可以把 Docker container 想成輕量級的虛擬機器,雖然嚴格來說並非如此。

前置需求

若要跟著我的範例操作,你需要準備以下工具:

我的範例應用程式

我為本教學建立了一個範例專案。這是一個極其簡單的網頁應用程式,以 Flask 框架的上傳文件為基礎。

請輸入以下指令來執行它:

git clone https://github.com/mtlynch/flask_upload_demo.git
cd flask_upload_demo
sudo pip install -r requirements.txt
gunicorn \
  demo.app:app \
  --bind 0.0.0.0:5000

這個應用程式讓你可以選擇檔案並上傳:

範例應用程式首頁截圖

flask-upload-demo 的上傳頁面

接著,它會在網址 http://[server address]:5000/uploads/[filename] 上永久提供該檔案:

範例應用程式上傳結果截圖

flask-upload-demo 正在提供已上傳的圖片

在伺服器內部,你可以看到應用程式已將檔案儲存到本機檔案系統的 demo/uploads 資料夾中:

$ ls -l demo/uploads/
-rw-rw-r-- 1 mike mike 230720 Nov 24 21:45 Space_Duck_Desktop_RGB_PNG.png

將範例應用程式 Docker 化

由於這個應用程式只有少數簡單的相依套件,要為它建立 Docker image 非常容易:

Dockerfile

FROM debian:stretch

RUN apt-get update
RUN apt-get install --yes \
      git-core \
      python \
      python-pip \
      python-virtualenv

# Create demo user system account.
ARG APP_USER="demo-user"
ARG APP_GROUP="demo-user"
ARG APP_HOME_DIR="/home/demo-user"
RUN set -x && \
    groupadd "$APP_GROUP" && \
    useradd \
      --comment "Demo app system account" \
      --home-dir "$APP_HOME_DIR" \
      --create-home \
      --system \
      --gid "$APP_GROUP" \
      "$APP_USER"

# Create directory for app source code.
ARG APP_ROOT="/srv/demo-app"
RUN mkdir --parents "$APP_ROOT" && \
    chown \
      --no-dereference \
      --recursive \
      "${APP_USER}:${APP_GROUP}" "$APP_ROOT"

USER "$APP_USER"
WORKDIR "$APP_ROOT"

# Install demo app.
ARG DEMO_APP_REPO="https://github.com/mtlynch/flask_upload_demo"
RUN set -x && \
    git clone "$DEMO_APP_REPO" . && \
    virtualenv VIRTUAL && \
    . VIRTUAL/bin/activate && \
    pip install --requirement requirements.txt

EXPOSE 5000

# Run demo app.
ENV FLASK_APP "demo/app.py"
CMD virtualenv VIRTUAL && \
    . VIRTUAL/bin/activate && \
    gunicorn \
      demo.app:app \
      --bind 0.0.0.0:5000 \
      --log-level info

上述 Dockerfile 執行了幾個主要任務來準備映像檔:

  1. 安裝 Git、Python 及相關套件
  2. 建立系統帳號(demo-user),以有限權限執行應用程式
  3. 在本地端複製應用程式原始碼儲存庫
  4. 加入 CMD 以在連接埠 5000 上啟動範例應用程式

你可以透過複製我的儲存庫並在本地端建置 Docker container 來測試這個 Dockerfile

cd ~
git clone https://github.com/mtlynch/docker-flask-upload-demo.git
cd docker-flask-upload-demo

docker build \
  --tag demo-app-image \
  .

docker run \
  --detach \
  --publish 80:5000 \
  --name demo-app \
  demo-app-image

若你在瀏覽器中造訪 http://localhost/,就會看到範例應用程式。

更貼近實務的 Docker 映像檔

大多數網頁應用程式不會直接從瀏覽器接收流量。相反地,它們會使用像 Nginx 或 Apache 這類 HTTP 伺服器來處理通用任務(例如負載平衡、提供靜態檔案),同時由後端處理應用程式專屬的邏輯。

我在 Docker 儲存庫中新增了nginx 分支,以展示更接近實際應用場景的 Docker image。相關的變更如下:

# Install nginx.
ARG NGINX_GROUP="www-data"
COPY nginx.conf /etc/nginx/sites-enabled/nginx.conf
RUN set -x && \
    apt-get install --yes \
      nginx \
      sudo && \
    rm /etc/nginx/sites-enabled/default && \
    usermod --append --groups "$NGINX_GROUP" "$APP_USER" && \
    echo "$APP_USER ALL=(ALL:ALL) NOPASSWD: /usr/sbin/nginx" >> /etc/sudoers

Nginx 通常以 root 身分執行,才能監聽 80 和 443 這類特權 HTTP 連接埠。因此,Dockerfile 使用 sudo 讓範例應用程式的使用者能以 root 身分啟動 Nginx,同時其他操作仍以有限權限執行。

最後,它會將 Nginx 設定檔複製到容器中:

nginx.conf

server {
    listen       80;
    server_name  example.org; # Replace with your server's domain name
    client_max_body_size 20m;

    # Serve static resources directly (bypass backend).
    location /uploads/ {
        alias /srv/demo-app/demo/uploads/;
    }

    # Forward all other requests to the application backend.
    location / {
        proxy_pass http://127.0.0.1:5000;
    }
}

location /uploads { ... } 區塊讓 Nginx 能直接從 /uploads 資料夾提供檔案,而無需將請求轉發給 flask-upload-demo 後端。這是一種常見的設定,因為 Nginx 處理靜態檔案的速度比應用程式後端更快、更有效率。

你可以使用以下指令來執行 flask-upload-demo 的 Nginx 版本:

cd ~/docker-flask-upload-demo
git checkout nginx

docker build \
  --tag demo-app-image \
  .

docker run \
  --detach \
  --publish 80:80 \
  --name demo-app \
  demo-app-image

應用程式同樣會出現在 http://localhost/。其行為與前一版本完全相同,差別在於提供上傳檔案時所耗用的資源更少。

準備 GCP 專案

在本地端部署應用程式固然不錯,但將其發佈到雲端、讓所有使用者都能存取,更加令人期待。你需要執行幾個步驟來設定系統,以便部署至 GCP。

首先,在 gcloud 中指定你的 GCP 專案名稱:

PROJECT_ID="ENTER-YOUR-PROJECT-ID-HERE"
gcloud config set project "$PROJECT_ID"

接著,使用 GCP 網頁主控台建立一個具有擁有者角色的服務帳戶

陷阱提醒:由於 GCP 中一個明顯的錯誤,若你使用根 GCP 帳戶(例如你的 @gmail.com 帳戶)或透過 gcloud 建立的服務帳戶,將 Docker image 推送至 gcr.io(見下一節)會失敗。

從 GCP 網頁主控台建立新服務帳戶的畫面截圖

從 GCP 網頁主控台建立新的服務帳戶

服務帳戶角色選擇畫面截圖

為服務帳戶指派角色

將私鑰下載為 key.json

服務帳戶私鑰下載畫面截圖

下載服務帳戶的私鑰

最後,使用 gcloud 以剛建立的服務帳戶進行驗證:

gcloud auth activate-service-account --key-file key.json

完成驗證後,你需要執行幾個指令,為本教學的後續步驟準備專案:

# Enable APIs you'll need for this workflow.
gcloud services enable \
  cloudresourcemanager.googleapis.com \
  compute.googleapis.com \
  containerregistry.googleapis.com \
  iam.googleapis.com

# Enable gcloud to provide credentials for Docker image pushes.
gcloud auth configure-docker --quiet

將映像檔上傳至 Google Container Registry

在將 Docker image 部署至 GCE 之前,你需要先將其發佈到 Docker 映像檔代管服務。GCR 是 Google Cloud 整合的 Docker 映像檔代管服務,因此是最簡便的選擇。

若要將 Docker image 上傳至 GCR,請取出我範例儲存庫的 nginx 分支:

cd ~/docker-flask-upload-demo
git checkout nginx

現在,在本地端建置 Docker image,並將映像檔推送至 GCR:

LOCAL_IMAGE_NAME="flask-upload-demo-image"
docker build --tag "$LOCAL_IMAGE_NAME" .

GCR_HOSTNAME="gcr.io"
GCR_IMAGE_PATH="${GCR_HOSTNAME}/${PROJECT_ID}/flask-demo-app"
docker tag "$LOCAL_IMAGE_NAME" "$GCR_IMAGE_PATH"
docker push "$GCR_IMAGE_PATH"

部署 Docker 容器

部署容器需要一點間接步驟。GCP 不允許你直接部署 Docker image。相反地,你需要使用 GCE 啟動一台完整的虛擬機器(VM),在該 VM 上執行 Docker,然後由 GCE 在該 VM 中的容器內執行你的 Docker image。幸好,GCP 的工具簡化了這個流程。

GCE VM 預設會阻擋 HTTP 輸入流量。若要允許,請建立一條防火牆規則,接受連接埠 80(明文 HTTP 流量的標準連接埠)上的 TCP 連線。

一個簡便的方法是將規則套用至任何帶有 http-server 標籤的 VM:

VM_TAGS="http-server"
gcloud compute \
  --project="$PROJECT_ID" \
  firewall-rules create default-allow-http \
  --direction=INGRESS \
  --action=ALLOW \
  --rules=tcp:80 \
  --target-tags="$VM_TAGS"

接著,使用 http-server 標籤部署你的 GCE VM:

VM_NAME="flask-demo-app-vm"
MACHINE_TYPE="n1-standard-1"
ZONE=us-east1-b
gcloud compute \
  --project="$PROJECT_ID" \
  instances create-with-container "$VM_NAME" \
  --zone="$ZONE" \
  --machine-type="$MACHINE_TYPE" \
  --network-tier=STANDARD \
  --metadata=google-logging-enabled=true \
  --maintenance-policy=MIGRATE \
  --scopes=https://www.googleapis.com/auth/cloud-platform \
  --tags="$VM_TAGS" \
  --image-family=cos-stable \
  --image-project=cos-cloud \
  --boot-disk-size=10GB \
  --boot-disk-type=pd-standard \
  --boot-disk-device-name="$VM_NAME" \
  --container-image="$GCR_IMAGE_PATH" \
  --container-restart-policy=on-failure

以下是幾個值得注意的旗標:

  --tags="$VM_TAGS" \

--tags 旗標會使用你為防火牆規則建立的標籤來啟動 VM。這個旗標可確保 VM 能在連接埠 80 上接收 HTTP 流量。

  --image-family=cos-stable \
  --image-project=cos-cloud \

這些旗標告訴 GCE 在Container-Optimized OS 之下執行容器,這是 Google 為執行 Docker 容器所打造的精簡版 Linux 作業系統。--image-family=cos-stable 會告訴 gcloud 使用最新穩定版的 Container-Optimized OS。

--container-image="$GCR_IMAGE_PATH" \

上述旗標會告訴 GCE 在 GCE VM 內要執行哪個 Docker image,使用的是你稍早建立的 GCR 網址。

指令完成後,你會看到類似以下的輸出:

Created [https://www.googleapis.com/compute/v1/projects/flask-upload-demo-2018-11-26/zones/us-east1-b/instances/flask-demo-app-vm].
NAME               ZONE        MACHINE_TYPE   PREEMPTIBLE  INTERNAL_IP  EXTERNAL_IP     STATUS
flask-demo-app-vm  us-east1-b  n1-standard-1               10.142.0.2   35.211.106.214  RUNNING

若你在瀏覽器中輸入 EXTERNAL_IP 的位址,其運作方式就和本機版本的應用程式一樣:

在 GCE 上執行的 flask-upload-demo 截圖

在 GCE 上執行的 flask-upload-demo

已上傳至 GCE 上 flask-upload-demo 的檔案截圖

flask-upload-demo 從 GCE 提供圖片檔案

問題在於,如果你終止該 VM 並使用相同的 Docker image 啟動新的 VM,先前上傳的檔案就消失了:

先前上傳檔案的 404 錯誤截圖

新的 GCE VM 無法提供該檔案,因為檔案儲存在先前的 VM 上

當然,這正是催生本教學的問題。容器將檔案儲存在其內部檔案系統中。當你終止主機 VM 時,就會遺失所有檔案。

為了解決這個問題,你需要將 Docker container 設定為將所有持久性資料儲存在 Google Cloud Storage(GCS)bucket 中。

規劃支援 GCS 的架構

以下是解決此問題的目標架構:

flask-demo-app 架構圖

在 Google Cloud Platform 上部署 Flask 應用程式的架構

  • 網頁瀏覽器只會與網頁伺服器 Nginx 溝通,Nginx 作為所有前端請求的協調者。
  • 如果網頁瀏覽器請求檔案,Nginx 會透過名為 gcsfuse 的公用程式從 GCS 取得檔案,該工具會將 GCS bucket 掛載為檔案系統中的資料夾。
  • 對於所有其他請求,Nginx 會將請求轉發給 flask-upload-demo 應用程式。
  • flask-upload-demo 也能透過 gcsfuse 公用程式將新檔案寫入 GCS。

此架構滿足了我在文章開頭定義的目標。VM 中的所有內容皆可拋棄,因為 GCS 儲存了所有永久狀態。所有應用程式碼都在 Docker container 中,這讓部署變得簡單且具原子性。

建立 GCS bucket(選用)

如果你還沒有 GCS bucket,可以使用以下 gcloud 指令建立一個:

STORAGE_LOCATION="us-east1"
GCS_BUCKET="${PROJECT_ID}-storage"
gsutil mb \
  -p "$PROJECT_ID" \
  -l "$STORAGE_LOCATION" \
  "gs://${GCS_BUCKET}"

否則,請將 GCS_BUCKET 環境變數設為你的 GCS bucket 名稱。

讓 Docker 容器存取 GCS

你需要修改 Docker image 以整合 gcsfuse 公用程式。幸好,我已經幫你做好了。完整的 Dockerfile 可在我 GitHub 儲存庫的 gcsfuse 分支上取得,但主要的變更如下:

# Install gcsfuse.
ARG GCSFUSE_REPO="gcsfuse-stretch"
ARG GCS_MOUNT_ROOT="/mnt/gcsfuse"
RUN set -x && \
    apt-get install --yes --no-install-recommends \
    ca-certificates \
    curl && \
    echo "deb http://packages.cloud.google.com/apt $GCSFUSE_REPO main" \
      | tee /etc/apt/sources.list.d/gcsfuse.list && \
    curl https://packages.cloud.google.com/apt/doc/apt-key.gpg \
      | apt-key add -
RUN apt-get update
RUN set -x && \
    apt-get install --yes gcsfuse && \
    echo 'user_allow_other' > /etc/fuse.conf && \
    mkdir --parents "$GCS_MOUNT_ROOT" && \
    chown \
      --no-dereference \
      "${APP_USER}:${NGINX_GROUP}" "$GCS_MOUNT_ROOT"

這裡有兩個值得討論的重點:

echo 'user_allow_other' > /etc/fuse.conf

上述這一行讓使用 gcsfuse-o allow_other 選項成為可能。這是必要的,因為應用程式系統帳號和 Nginx 系統帳號都需要存取 GCS 資料夾。若設定檔中沒有 user_allow_other 這一行,就只有單一帳號能存取 GCS 資料夾。

陷阱提醒:如果有多個系統帳號需要存取 GCS bucket,/etc/fuse.conf 檔案必須包含 user_allow_other 這一行。

mkdir --parents "$GCS_MOUNT_ROOT" && \
chown \
  --no-dereference \
  "${APP_USER}:${NGINX_GROUP}" "$GCS_MOUNT_ROOT"

gcsfuse 需要一個已存在且啟動使用者具備寫入權限的目錄。一般使用者對 /mnt 目錄沒有寫入權限,因此 Dockerfileroot 使用者身分建立 /mnt/gcsfuse 目錄,並使用 chown 將擁有權指派給 Nginx 與範例應用程式的系統帳號。

其他值得注意的變更位於 DockerfileCMD 區段,該區段定義了容器的執行階段行為:

ENV GCS_BUCKET "REPLACE-WITH-YOUR-GCS-BUCKET-NAME"
ENV GCS_MOUNT_ROOT "$GCS_MOUNT_ROOT"
ENV APP_UPLOADS_DIR "${APP_ROOT}/demo/uploads"

CMD set -x && \
    sudo nginx && \
    gcsfuse \
      -o nonempty \
      -o allow_other \
      --implicit-dirs \
      "$GCS_BUCKET" "$GCS_MOUNT_ROOT" && \
    if [ ! -d "$APP_UPLOADS_DIR" ]; then \
      ln --symbolic "$GCS_MOUNT_ROOT" "$APP_UPLOADS_DIR"; \
    fi && \
    virtualenv VIRTUAL && \
    . VIRTUAL/bin/activate && \
    gunicorn \
      demo.app:app \
      --bind 127.0.0.1:5000 \
      --log-level info

同樣地,我們來逐段拆解值得注意的片段:

gcsfuse \
  -o nonempty \
  -o allow_other \
  --implicit-dirs \
  "$GCS_BUCKET" "$GCS_MOUNT_ROOT"

如同上述的 user_allow_other 選項,-o allow_other 讓多個使用者能夠存取已掛載的資料夾,因為 nginx(以 www-data 使用者身分執行)和範例應用程式(以 demo-user 身分執行)都需要存取權。

陷阱提醒:如果多個使用者帳號會存取 GCS 掛載中的檔案,gcsfuse 需要加上 -o allow_other 旗標。

若沒有--implicit-dirs 旗標,gcsfuse 就無法存取 GCS bucket 子資料夾中的檔案。

陷阱提醒:如果 GCS bucket 包含子資料夾,gcsfuse 需要加上 --implicit-dirs 旗標。

if [ ! -d "$APP_UPLOADS_DIR" ]; then \
  ln --symbolic "$GCS_MOUNT_ROOT" "$APP_UPLOADS_DIR"; \
fi

應用程式會將上傳的檔案寫入 demo/uploads 目錄。上述區塊會建立一個從 demo/uploads/mnt/gcsfuse 的符號連結。如此一來,當應用程式以為自己寫入 demo/uploads 路徑時,實際上是寫入你的 GCS bucket。if/then 區塊可避免重複執行此步驟,例如在容器重新啟動時。

建立具備 GCS 存取權限的服務帳戶

在將此映像檔部署至 GCE 之前,還有一個額外步驟。預設情況下,GCE 執行個體會在標準 GCE 服務帳戶的 context 下執行。該帳戶對 GCS 僅有唯讀存取權,因此應用程式將無法寫入新檔案至 GCS。

為了解決這個問題,請建立一個具備以下兩種角色的自訂服務帳戶:

  • storage.objectAdmin:允許 VM 中的處理程序讀寫 GCS 中的物件。
  • logging.logWriter:允許應用程式的日誌輸出顯示在 GCP 的日誌介面中。

陷阱提醒:除非你使用自訂服務帳戶啟動 GCE 執行個體,否則它們無法寫入 GCS bucket。

以下指令會建立一個具備必要權限的服務帳戶:

SERVICE_ACCOUNT_NAME=flask-demo-app-service-account
SERVICE_ACCOUNT_EMAIL="${SERVICE_ACCOUNT_NAME}@${PROJECT_ID}.iam.gserviceaccount.com"

gcloud iam service-accounts create "$SERVICE_ACCOUNT_NAME"
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
  --member "serviceAccount:${SERVICE_ACCOUNT_EMAIL}" \
  --role roles/storage.objectAdmin
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
  --member "serviceAccount:${SERVICE_ACCOUNT_EMAIL}" \
  --role roles/logging.logWriter

部署支援 GCS 的容器

回到你複製的 docker-flask-upload-demo 儲存庫,並取出 gcsfuse 分支:

cd ~/docker-flask-upload-demo
git checkout gcsfuse

現在,重新建置 Docker image 並將其推送至 GCR:

LOCAL_IMAGE_NAME="flask-upload-demo-image"
docker build --tag "$LOCAL_IMAGE_NAME" .

GCR_HOSTNAME="gcr.io"
GCR_IMAGE_PATH="${GCR_HOSTNAME}/${PROJECT_ID}/flask-demo-app"
docker tag "$LOCAL_IMAGE_NAME" "$GCR_IMAGE_PATH"
docker push "$GCR_IMAGE_PATH"

注意:如果你嘗試在本地端執行此映像檔,將會失敗。gcsfuse 版本的 Dockerfile 假設其執行環境已通過 gcloud 驗證(這對於在 GCE 上執行的容器而言是成立的)。雖然可以調整 Dockerfile,使其在 GCE 之外執行時也能掛載 GCS bucket,但這就留給讀者自行練習了。

有了新的自訂 GCE 服務帳戶,你就可以準備部署支援 GCS 的容器了:

VM_NAME="flask-demo-app-vm-gcsfuse"
MACHINE_TYPE="n1-standard-1"
ZONE=us-east1-b
gcloud compute \
  --project="$PROJECT_ID" \
  instances create-with-container "$VM_NAME" \
  --zone="$ZONE" \
  --machine-type="$MACHINE_TYPE" \
  --network-tier=STANDARD \
  --metadata=google-logging-enabled=true \
  --maintenance-policy=MIGRATE \
  --scopes=https://www.googleapis.com/auth/cloud-platform \
  --service-account="$SERVICE_ACCOUNT_EMAIL" \
  --tags="$VM_TAGS" \
  --image-family=cos-stable \
  --image-project=cos-cloud \
  --boot-disk-size=10GB \
  --boot-disk-type=pd-standard \
  --boot-disk-device-name="$VM_NAME" \
  --container-image="$GCR_IMAGE_PATH" \
  --container-restart-policy=on-failure \
  --container-privileged \
  --container-env="GCS_BUCKET=$GCS_BUCKET"

此指令與先前的部署指令相同,但多了兩個旗標:

  --container-privileged \

--container-privileged 旗標是必要的,以允許 Docker 容器掛載 FUSE 檔案系統(Docker 提供了更細緻的達成方式,但 GCE 目前尚未支援)。

陷阱提醒:除非你使用 --container-privileged 旗標部署 VM,否則 gcsfuse 無法在 GCE 上掛載 GCS bucket。

  --container-env="GCS_BUCKET=$GCS_BUCKET"

我刻意將 Docker image 設計為在執行階段之前無需知道 GCS bucket 的名稱。--container-env 旗標讓你能在部署時指定 GCS_BUCKET 環境變數。

持久化的回報……正是持久性

你終於擁有了一個能將狀態持久化至 GCS 的 Docker container。你可以透過上傳檔案至已部署的應用程式來測試:

flask-upload-demo 從 GCS 上的永久儲存空間提供檔案的截圖

flask-upload-demo 從 GCS 上的永久儲存空間提供檔案

如果你查看 GCS bucket,就會看到剛上傳的檔案:

顯示 GCS bucket 中已上傳檔案的截圖

位於 GCS bucket 中的圖片檔案

真正的考驗在於此狀態是否能在不同的 VM 之間持續存在。你可以透過完全終止 VM 並重新部署來驗證。來自先前 VM 的圖片網址在新伺服器上仍可存取:

flask-upload-demo 從不同 IP 提供檔案的截圖

即使 VM 已被銷毀並重建,flask-upload-demo 仍持續提供檔案

加碼:日誌介面

這個解決方案附帶的一個好處是,GCP 提供了一個精美易用的網頁介面來檢視應用程式的日誌。

你隨時可以透過 SSH 連線至 VM,然後執行 docker logs 來手動查看日誌:

$ docker logs klt-flask-demo-app-vm-gcsfuse-qnnf
...
[2018-11-26 21:28:54 +0000] [33] [INFO] Starting gunicorn 19.9.0
[2018-11-26 21:28:54 +0000] [33] [INFO] Listening at: http://127.0.0.1:5000 (33)
[2018-11-26 21:28:54 +0000] [33] [INFO] Using worker: sync
[2018-11-26 21:28:54 +0000] [37] [INFO] Booting worker with pid: 37
[2018-11-26 21:32:59 +0000] [37] [INFO] Saving uploaded file "zestful-logo.png" to "/srv/demo-app/demo/uploads/zestful-logo.png"

更簡便的方法是開啟 GCP 的 StackDriver 日誌介面。在那裡,你會在功能豐富的網頁介面中找到所有日誌:

GCP StackDriver 介面中的應用程式日誌

GCP 的 StackDriver 介面顯示來自應用程式的日誌輸出。

推送新版本

此架構讓推送新版本變得容易。任何時候你想更新應用程式或其相依套件,只要建置新的映像檔並推送至 GCR 即可:

docker build --tag "$LOCAL_IMAGE_NAME" .
docker tag "$LOCAL_IMAGE_NAME" "$GCR_IMAGE_PATH"
docker push "$GCR_IMAGE_PATH"

接著,使用 update-container 指令來更新執行中 GCE 執行個體上的 Docker image:

gcloud compute \
  --project="$PROJECT_ID" \
  instances update-container "$VM_NAME" \
  --container-image="$GCR_IMAGE_PATH"

陷阱提醒:除非你已指派靜態 IP,否則執行此指令後,VM 的外部 IP 位址將會變更。

如果你不小心推送了有問題的版本,update-container 指令可讓你回滾至先前已知良好的映像檔。

限制

此解決方案與 gcsfuse 公用程式及 GCS 本身有相同的限制。gcsfuse 盡其所能讓 GCS bucket 看起來像一般的檔案系統資料夾,但在兩個主要方面,這種抽象化仍會失效:

  • GCS 不支援鎖定,因此如果你的應用程式嘗試取得檔案鎖定,情況會變得不穩定。
  • 延遲偏高,尤其是在對大型檔案進行小型、隨機讀寫時。

特別是,我發現若將 sqlite 指向位於 gcsfuse 掛載上的資料庫,它很快就會失敗。

結論

在本教學中,你學會了如何在不修改應用程式本身的情況下,將應用程式的資料重新導向至雲端儲存。範例應用程式本身對 Google Cloud Platform 毫無所知,但你仍將其部署至 Google Compute Engine,並將其持久性資料重新導向至 Google Cloud Storage。

原始碼

  • flask-upload-demo:將狀態保留在本機檔案系統中的 Flask 範例應用程式。
  • docker-flask-upload-demo:flask-upload-demo 的 Docker 設定,共有三種版本:
    • master 分支 - 展示應用程式的基本封裝
    • nginx 分支 - 展示更貼近真實世界的架構,其中 nginx 為應用程式代理流量
    • gcsfuse 分支 - 展示如何從 Docker 容器內部掛載 Google Cloud Storage bucket(假設容器在具備 Google Cloud Storage 讀寫權限的 Google Compute Engine VM 中執行)。
  • mediagoblin-docker (gcsfuse 分支) - 一個真實媒體分享應用程式的 Docker 設定,我在其中使用相同技術將應用程式的永久資料重新導向至 Google Cloud Storage。

軟體架構圖由 Loraine Yow(蘿蘭·尤)繪製

原文由 Michael Lynch 發布

本文章由 muse-spark-1.2-contributor 進行翻譯