零程式碼修改,為應用程式加裝雲端儲存
我最近在一台伺服器上安裝了一款媒體分享應用程式。安裝過程很簡單,但它卻為長期維護埋下了一個棘手的陷阱。
每當使用者上傳檔案時,網頁應用程式就會將檔案儲存到本機檔案系統中。如果我哪天重灌或重建伺服器,就必須手動備份並還原所有檔案。更好的架構是讓應用程式將檔案寫入獨立的儲存伺服器,但我不想為此花上數個月重寫應用程式。
透過 Docker、Google Cloud Storage 以及 gcsfuse 公用程式,我在完全沒有修改應用程式任何一行程式碼的情況下,實現了這種分離。
在本教學中,我將示範如何在盡量不更動原始碼的前提下,為舊有應用程式加裝 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 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 執行了幾個主要任務來準備映像檔:
- 安裝 Git、Python 及相關套件
- 建立系統帳號(
demo-user),以有限權限執行應用程式 - 在本地端複製應用程式原始碼儲存庫
- 加入
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/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 處理靜態檔案的速度比應用程式後端更快、更有效率。
你可以使用以下指令來執行 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 網頁主控台建立新的服務帳戶

為服務帳戶指派角色
將私鑰下載為 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

flask-upload-demo 從 GCE 提供圖片檔案
問題在於,如果你終止該 VM 並使用相同的 Docker image 啟動新的 VM,先前上傳的檔案就消失了:

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

在 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 目錄沒有寫入權限,因此 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 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 上的永久儲存空間提供檔案
如果你查看 GCS bucket,就會看到剛上傳的檔案:

位於 GCS bucket 中的圖片檔案
真正的考驗在於此狀態是否能在不同的 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 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(蘿蘭·尤)繪製
隨機一篇部落格

