Retrofitting Apps for Cloud Storage with Zero Code Changes

Michael Lynch

コードを一切変えずにアプリをクラウドストレージに対応させる

原文は Michael Lynch により に公開されました。 このブログを購読する

最近、サーバーの1台にメディア共有アプリをインストールしました。インストール自体は簡単でしたが、長期的な運用を考えると厄介な落とし穴が潜んでいました。

ユーザーがファイルをアップロードするたびに、Webアプリはそれをローカルのファイルシステムに保存していました。もしサーバーを作り直すことになれば、すべてのファイルを手作業でバックアップして復元しなければなりません。本来はアプリがファイルを別のストレージサーバーに書き込むアーキテクチャが理想ですが、そのために何ヶ月もかけてアプリを書き換える気にはなれませんでした。

単純なアーキテクチャと理想的なアーキテクチャの比較

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イメージとは、OSやサードパーティの依存関係を含め、アプリの実行に必要なすべてのファイル一式のことです。
  • Dockerコンテナとは、Dockerイメージが実行されるライブ環境のことです。

このチュートリアルでは厳密にはそうではありませんが、Dockerコンテナを軽量な仮想マシンだと考えてもらって構いません。

前提条件

私のサンプルを実際に試すには、以下が必要です。

サンプルアプリ

このチュートリアル用にサンプルプロジェクトを作成しました。Flaskフレームワークのアップロードに関するドキュメントをベースにした、ごくシンプルなWebアプリです。

実行するには、以下のコマンドを入力します。

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]というURLで恒久的に配信されます。

デモアプリのアップロード結果画面のスクリーンショット

アップロードされた画像を配信する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は、イメージを準備するために主に以下の処理を行っています。

  1. Git、Pythonおよび関連パッケージをインストールする
  2. 権限を制限してアプリを実行するためのシステムアカウント(demo-user)を作成する
  3. アプリのソースリポジトリをローカルにクローンする
  4. ポート5000でデモアプリを起動するためのCMDを追加する

このDockerfileは、私のリポジトリをクローンして、ローカルでDockerコンテナをビルドすることでテストできます。

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イメージ

ほとんどのWebアプリは、ブラウザからのトラフィックを直接受け付けません。代わりにNginxやApacheのようなHTTPサーバーが、ロードバランシングや静的ファイルの配信といった汎用的な処理を担い、バックエンドがアプリ固有のロジックを処理します。

より実践に近いDockerイメージを示すため、Dockerリポジトリにnginxブランチを追加しました。主な変更点は以下のとおりです。

# 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は通常、特権ポートである80番や443番でリッスンするためにrootとして実行されます。そのため、このDockerfileではsudoを使って、デモアプリ用ユーザーがNginxをrootとして起動できるようにしつつ、それ以外の処理はすべて権限を制限した状態で行うようにしています。

最後に、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のウェブコンソールでサービスアカウントをオーナー権限で作成します。

要注意: GCPの明らかなバグにより、ルートのGCPアカウント(例:@gmail.comアカウント)やgcloud経由で作成したサービスアカウントを使用すると、gcr.ioへのDockerイメージプッシュ(次のセクションを参照)が失敗します。

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イメージを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を実行し、さらにそのVM内のコンテナでDockerイメージを実行します。幸い、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を起動します。これにより、ポート80でのHTTPトラフィックを受信できるようになります。

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

これらのフラグは、GCEにContainer-Optimized OS上でコンテナを実行するよう指示します。これはGoogleがDockerコンテナ実行用に作成した軽量なLinux OSです。--image-family=cos-stableは、Container-Optimized OSの最新の安定版を使用するようgcloudに指示します。

--container-image="$GCR_IMAGE_PATH" \

上記のフラグは、GCEのVM内で実行するDockerイメージを、先ほど作成したGCRのURLを使って指定します。

コマンドが完了すると、次のような出力が表示されます。

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でアップロードされたファイルを表示するスクリーンショット

GCE上のflask-upload-demoで画像ファイルを配信する様子

問題は、そのVMを削除して同じDockerイメージで新しいVMを起動すると、アップロードしたファイルが消えてしまうことです。

以前アップロードしたファイルが404エラーになるスクリーンショット

新しいGCE VMはファイルを配信できません。ファイルが以前のVMに保存されていたためです

もちろん、これこそがこのチュートリアル全体のきっかけとなった問題です。コンテナはファイルを内部のファイルシステムに保存します。ホストVMを終了すると、すべてのファイルが失われてしまいます。

これを解決するには、Dockerコンテナがすべての永続データをGoogle Cloud Storage(GCS)バケットに保存するように構成する必要があります。

GCS対応アーキテクチャの設計

この問題を解決する目標のアーキテクチャは次のとおりです。

flask-demo-appのアーキテクチャ図

FlaskアプリをGoogle Cloud Platformにデプロイするためのアーキテクチャ

  • WebブラウザはWebサーバーであるNginxとのみ通信し、Nginxがフロントエンドからのすべてのリクエストを統括します。
  • Webブラウザがファイルをリクエストすると、Nginxはgcsfuseというユーティリティを介してGCSからファイルを取得します。gcsfuseはGCSバケットをファイルシステム上のフォルダとしてマウントします。
  • それ以外のリクエストについては、Nginxがflask-upload-demoアプリにリクエストを転送します。
  • flask-upload-demoも同じくgcsfuseユーティリティを介してGCSに新しいファイルを書き込むことができます。

このアーキテクチャは、冒頭で定義した目標を満たしています。永続的な状態はすべてGCSに保存されるため、VM内のすべてが使い捨て可能です。すべてのアプリコードは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へのアクセスを許可する

gcsfuseユーティリティを統合するためにDockerイメージを変更する必要があります。幸い、私がすでに対応済みです。完全なDockerfileGitHubリポジトリの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"

ここで注目すべき点が2つあります。

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とデモアプリのシステムアカウントに所有権を割り当てています。

もう一つの注目すべき変更点は、コンテナの実行時の動作を定義する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バケットのサブフォルダ内のファイルにアクセスできません。

要注意: 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に新しいファイルを書き込もうとすると失敗します。

これを解決するには、次の2つのロールを持つカスタムサービスアカウントを作成します。

  • 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上で動作するコンテナではこれは当てはまります)。GCEの外で実行しながらGCSバケットをマウントするようにDockerfileを調整することも可能ですが、それは読者への課題とします。

新しいカスタム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"

このコマンドは前回のデプロイコマンドと同じですが、2つのフラグが追加されています。

  --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コンテナが完成しました。デプロイしたアプリにファイルをアップロードしてテストできます。

GCS上の永続ストレージからファイルを配信するflask-upload-demoのスクリーンショット

GCS上の永続ストレージからファイルを配信するflask-upload-demo

GCSバケットを確認すると、アップロードしたばかりのファイルが表示されます。

GCSバケット内の画像ファイルを示すスクリーンショット

GCSバケット内の画像ファイル

本当のテストは、この状態が異なるVM間でも永続するかどうかです。VMを完全に削除して再デプロイすることで検証できます。以前のVMでの画像URLが、新しいサーバーでも引き続きアクセス可能です。

異なるIP上でファイルを配信するflask-upload-demoのスクリーンショット

VMを破棄して再構築した後も、ファイルの配信を続けるflask-upload-demo

おまけ:ロギングインターフェース

このソリューションの嬉しい副次的な利点として、GCPがアプリのログを表示するための洗練されたWebインターフェースを提供してくれることが挙げられます。

いつでもVMにSSHで接続し、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ロギングインターフェースを開くことです。そこでは、機能豊富なWebインターフェースですべてのログを確認できます。

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バケットを通常のファイルシステムフォルダのように見せかけようと懸命に努力しますが、抽象化は主に2つの点で破綻します。

  • GCSはロックをサポートしていないため、アプリがファイルロックを取得しようとすると問題が発生します。
  • レイテンシが高く、特に大きなファイルに対する小さなランダムな読み書きを行う際に顕著です。

特に、sqliteをgcsfuseマウント上のデータベースに向けて使用すると、すぐに失敗することを確認しています。

まとめ

このチュートリアルでは、アプリ自体に変更を加えることなく、アプリのデータをクラウドストレージにリダイレクトする方法を学びました。サンプルアプリはGoogle Cloud Platformをまったく意識していませんでしたが、それをGoogle Compute Engineにデプロイし、永続データをGoogle Cloud Storageにリダイレクトしました。

ソースコード

  • flask-upload-demo: ローカルファイルシステムに状態を保持するFlaskサンプルアプリ
  • docker-flask-upload-demo: flask-upload-demo用のDocker構成。3つのバリエーションがあります。
    • masterブランチ - アプリの基本的なパッケージ化を示します
    • nginxブランチ - nginxがアプリのトラフィックをプロキシする、より現実的なアーキテクチャを示します
    • gcsfuseブランチ - Dockerコンテナ内からGoogle Cloud Storageバケットをマウントする方法を示します(コンテナがGoogle Cloud Storageへの読み書き権限を持つGoogle Compute Engine VMで実行されることを前提としています)。
  • mediagoblin-docker(gcsfuseブランチ) - 同じ手法を使ってアプリの永続データをGoogle Cloud Storageにリダイレクトした、実世界のメディア共有アプリ用Docker構成

ソフトウェアアーキテクチャ図:Loraine Yow作

この記事は「muse-spark-1.2-contributor」を使用して翻訳されました。

コメント