Retrofitting Apps for Cloud Storage with Zero Code Changes

Michael Lynch

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

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

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

単純な構成と理想的な構成の比較

DockerとGoogle Cloud Storage、そしてgcsfuseユーティリティを使うことで、アプリのコードを1行も変更することなく、この分離を実現できました。

このチュートリアルでは、ソースコードへの変更を最小限に抑えながら、レガシーなアプリを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://[サーバーアドレス]:5000/uploads/[ファイル名]という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はリクエストをflask-upload-demoのバックエンドに転送するのではなく、/uploadsフォルダからファイルを直接配信できるようになります。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の不具合と思われる挙動により、gcr.ioへのDockerイメージプッシュ(次のセクションを参照)が、ルートのGCPアカウント(@gmail.comアカウントなど)やgcloud経由で作成したサービスアカウントを使うと失敗します。

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を実行し、さらにその中で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上でコンテナを実行するよう指示します。これはDockerコンテナ実行用にGoogleが作った最小構成のLinux OSです。--image-family=cos-stableは、Container-Optimized OSの最新の安定版を使うようgcloudに指示します。

--container-image="$GCR_IMAGE_PATH" \

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

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

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にそのバケット名を設定してください。

DockerコンテナにGCSへのアクセス権を付与する

Dockerイメージを修正してgcsfuseユーティリティを組み込む必要があります。幸い、その作業はすでに済ませてあります。完全な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コンテナが完成しました。デプロイしたアプリにファイルをアップロードして試してみましょう。

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

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

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

アップロードされたファイルが表示されたGCSバケットのスクリーンショット

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

本当の試金石は、この状態が異なるVM間でも保持されるかどうかです。VMを完全に削除して再デプロイすることで確認できます。以前のVMでの画像URLが、新しいサーバーでもそのままアクセス可能です。

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

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

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

このソリューションの嬉しい副次的効果として、GCPがアプリのログを確認できる洗練されたウェブインターフェースを提供してくれることが挙げられます。

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

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はロックをサポートしていないため、アプリがファイルロックを取得しようとすると不具合が生じます。
  • レイテンシが高く、特に大きなファイルに対して小さなランダムな読み書きを行う場合に顕著です。

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

まとめ

このチュートリアルでは、アプリ自体に変更を加えることなく、アプリのデータをクラウドストレージにリダイレクトする方法を学びました。サンプルアプリは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ブランチ) - 実際のメディア共有アプリ用のDocker構成で、同じ手法を使ってアプリの永続データをGoogle Cloud Storageにリダイレクトした例です。

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

原文は Michael Lynch により に公開されました。

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