不改一行程式碼,為應用程式加裝雲端儲存
原文由 Michael Lynch 于 發布,訂閱此部落格
我最近在一台伺服器上安裝了一套媒體分享應用程式。安裝很簡單,但它卻為長期的維護埋下了一個棘手的陷阱。
使用者每次上傳檔案時,這個網頁應用程式都會把它存到本地檔案系統。如果哪天我把伺服器整個砍掉重建,就得手動備份並還原所有檔案。更好的架構是讓應用程式把檔案寫到獨立的儲存伺服器上,但我可不想為此花上好幾個月重寫程式。
透過 Docker、Google Cloud Storage 和 gcsfuse 這個工具,我在完全沒有修改應用程式任何一行程式碼的情況下,實現了這種分離。
在這篇教學中,我會示範如何在盡量不更動原始碼的前提下,為舊有應用程式 retrofit,使其支援 Google Cloud Storage。
專有名詞定義
如果你對 Google Cloud Platform 還不熟悉,以下是這篇教學會用到的幾個縮寫:
| 縮寫 | 全名 | 是什麼? | 類似於 |
|---|---|---|---|
| GCS | Google Cloud Storage | Google 的雲端儲存服務 | Amazon S3 |
| GCE | Google Compute Engine | Google 的隨需虛擬機器服務 | Amazon EC2 |
| GCR | Google Container Registry | Google 的 Docker 映像檔託管服務 | Docker Hub |
| GCP | Google Cloud Platform | Google 的雲端運算平台(GCS、GCE 和 GCR 都是 GCP 的一部分) | Amazon Web Services 或 Microsoft Azure |
Docker 讓開發者能為應用程式打造可隨處執行的自給自足環境:
- 一個 Docker 映像檔是執行應用程式所需的所有檔案集合,包含作業系統與所有第三方相依套件。
- 一個 Docker 容器則是 Docker 映像檔實際執行的動態環境。
在這篇教學中,你可以把 Docker 容器想成輕量級的虛擬機器,雖然嚴格來說並非如此。
前置需求
要跟著我的範例實作,你需要準備以下工具:
- Google Cloud SDK
- Docker(免費的Community Edition 就可以了)
範例應用程式
我為這篇教學建立了一個範例專案。這是一個極為簡單的網頁應用程式,基於 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 映像檔很容易:
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 執行了幾個主要步驟來準備映像檔:
- 安裝 Git、Python 及相關套件
- 建立一個系統帳號(
demo-user),以受限權限來執行應用程式 - 在本地複製應用程式原始碼儲存庫
- 加入
CMD指令,在 port 5000 上啟動範例應用程式
你可以透過複製我的儲存庫並在本地建置 Docker 容器來測試這個 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 映像檔。相關的變更如下:
# 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/sudoersNginx 通常以 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 處理靜態檔案的速度比應用程式後端更快、更有效率。
你可以用以下指令來執行 Nginx 版本的 flask-upload-demo:
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 網頁主控台建立一個具有擁有者(owner)角色的服務帳號:
地雷提醒:由於GCP 上一個明顯的錯誤,如果你使用 GCP 的根帳號(例如你的 @gmail.com 帳號)或透過 gcloud 建立的服務帳號,將 Docker 映像檔推送到 gcr.io(見下一節)時會失敗。

從 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 映像檔部署到 GCE 之前,你需要先將它發佈到 Docker 映像檔託管服務。GCR 是 Google Cloud 內建的 Docker 映像檔託管服務,因此是最簡便的選擇。
要將 Docker 映像檔上傳到 GCR,請切換到我範例儲存庫的 nginx 分支:
cd ~/docker-flask-upload-demo
git checkout nginx接著在本地建置 Docker 映像檔,並將其推送到 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 映像檔。相反地,你要透過 GCE 建立一台完整的虛擬機器(VM),在該 VM 上執行 Docker,再由 GCE 在該 VM 中的容器內執行你的 Docker 映像檔。幸好,GCP 的工具讓這個流程變得簡單許多。
GCE 的 VM 預設會阻擋所有傳入的 HTTP 流量。要允許流量,你需要建立一條防火牆規則,接受 port 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。這個參數確保它能接收 port 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 映像檔,使用的是你稍早建立的 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

flask-upload-demo 從 GCE 提供圖片檔案
問題在於,如果你終止那台 VM 並用同樣的 Docker 映像檔啟動一台新的 VM,你上傳的檔案就不見了:

新的 GCE VM 無法提供該檔案,因為檔案儲存在上一台 VM 上
當然,這正是催生整篇教學的問題所在。容器將檔案儲存在其內部的檔案系統中。當你終止宿主 VM 時,所有檔案都會遺失。
為了解決這個問題,你需要設定 Docker 容器,將所有需要持久保存的資料都儲存到 Google Cloud Storage(GCS)儲存桶中。
規劃支援 GCS 的架構
以下是解決這個問題的目標架構:

在 Google Cloud Platform 上部署 Flask 應用程式的架構
- 網頁瀏覽器只與網頁伺服器 Nginx 溝通,由 Nginx 作為所有前端請求的協調者。
- 如果網頁瀏覽器請求的是檔案,Nginx 會透過名為gcsfuse 的工具從 GCS 取得檔案,該工具會將 GCS 儲存桶掛載為檔案系統上的資料夾。
- 對於其他所有請求,Nginx 會將請求轉發給 flask-upload-demo 應用程式。
- flask-upload-demo 也能透過 gcsfuse 工具將新檔案寫入 GCS。
這個架構滿足了我在文章開頭定義的目標。VM 中的所有東西都是可拋棄的,因為 GCS 儲存了所有需要永久保存的狀態。所有應用程式碼都在 Docker 容器中,這讓部署變得簡單且具原子性。
建立 GCS 儲存桶(選用)
如果你還沒有 GCS 儲存桶,可以用以下 gcloud 指令來建立一個:
STORAGE_LOCATION="us-east1"
GCS_BUCKET="${PROJECT_ID}-storage"
gsutil mb \
-p "$PROJECT_ID" \
-l "$STORAGE_LOCATION" \
"gs://${GCS_BUCKET}"否則,請將 GCS_BUCKET 環境變數設定為現有 GCS 儲存桶的名稱。
讓 Docker 容器存取 GCS
你需要修改 Docker 映像檔以整合 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 儲存桶,/etc/fuse.conf 檔案就必須包含 user_allow_other 這一行。
mkdir --parents "$GCS_MOUNT_ROOT" && \
chown \
--no-dereference \
"${APP_USER}:${NGINX_GROUP}" "$GCS_MOUNT_ROOT"gcsfuse 需要一個啟動使用者具備寫入權限的現有目錄。一般使用者對 /mnt 目錄沒有寫入權限,因此 Dockerfile 以 root 使用者身分建立 /mnt/gcsfuse 目錄,並使用 chown 將擁有權指派給 Nginx 和範例應用程式的系統帳號。
另一個有趣的變更是在 Dockerfile 的 CMD 區段,它定義了容器的執行階段行為:
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 儲存桶子資料夾中的檔案。
地雷提醒:如果 GCS 儲存桶包含子資料夾,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 儲存桶。if/then 區塊則可避免重複執行這個步驟,例如在容器重新啟動時。
建立具備 GCS 存取權限的服務帳號
在將這個映像檔部署到 GCE 之前,還有一個額外的步驟。預設情況下,GCE 執行個體會在標準 GCE 服務帳號的脈絡下執行。該帳號對 GCS 只有唯讀存取權限,因此應用程式將無法寫入新檔案到 GCS。
為了解決這個問題,請建立一個具備以下兩種角色的自訂服務帳號:
storage.objectAdmin:允許 VM 中的處理程序讀寫 GCS 中的物件。logging.logWriter:允許應用程式的記錄檔輸出顯示在 GCP 的記錄介面中。
地雷提醒:除非你使用自訂服務帳號來啟動 GCE 執行個體,否則它無法寫入 GCS 儲存桶。
以下指令會建立一個具備必要權限的服務帳號:
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 映像檔並將其推送到 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 儲存桶,但這就留給讀者自行練習了。
有了新的自訂 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 儲存桶。
--container-env="GCS_BUCKET=$GCS_BUCKET"我刻意將 Docker 映像檔設計成在執行時期之前都不需要知道 GCS 儲存桶的名稱。--container-env 旗標讓你可以在部署時指定 GCS_BUCKET 環境變數。
持久化的回報,就是真正的持久
你終於擁有了一個會將狀態持久保存到 GCS 的 Docker 容器。你可以透過上傳檔案到已部署的應用程式來測試:

flask-upload-demo 從 GCS 上的永久儲存空間提供檔案
如果你查看 GCS 儲存桶,就會看到你剛上傳的檔案:

GCS 儲存桶中的圖片檔案
真正的考驗在於,這個狀態是否能在不同的 VM 之間持續存在。你可以透過完全終止 VM 並重新部署來驗證。來自上一台 VM 的圖片網址,在新的伺服器上依然可以存取:

即使 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 介面顯示了來自應用程式的記錄輸出。
發佈新版本
這個架構讓發佈新版本變得非常容易。每當你想更新應用程式或其相依套件時,只要建置新的映像檔並推送到 GCR 即可:
docker build --tag "$LOCAL_IMAGE_NAME" .
docker tag "$LOCAL_IMAGE_NAME" "$GCR_IMAGE_PATH"
docker push "$GCR_IMAGE_PATH"接著,使用 update-container 指令來更新執行中 GCE 執行個體上的 Docker 映像檔:
gcloud compute \
--project="$PROJECT_ID" \
instances update-container "$VM_NAME" \
--container-image="$GCR_IMAGE_PATH"地雷提醒:除非你已指派靜態 IP,否則執行這個指令後,VM 的外部 IP 位址將會改變。
如果你不小心推送了有問題的版本,update-container 指令還能讓你回復到先前已知可正常運作的映像檔。
限制
這個解決方案與 gcsfuse 工具及 GCS 本身有著相同的限制。gcsfuse 竭盡全力讓 GCS 儲存桶看起來像一般的檔案系統資料夾,但在兩個主要方面,這個抽象化還是會破功:
- 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 儲存桶(假設容器在具備 Google Cloud Storage 讀寫權限的 Google Compute Engine VM 中執行)。
- mediagoblin-docker(gcsfuse 分支) - 一個真實世界的媒體分享應用程式的 Docker 設定,我在其中使用了相同的技巧,將應用程式的永久資料重新導向到 Google Cloud Storage。
軟體架構圖由 Loraine Yow 繪製
隨機一篇部落格


留言
登入後參與討論