【Flask】アプリをDocker化して、Artifact RegistryにPushしてCloud Runにデプロイする方法

この記事は約15分で読めます。

前回こちらの記事で、flaskアプリケーションを作成し、ローカルで確認するところまでを行った。

今回は、FlaskアプリケーションをDocker化してCloud Runにデプロイする部分を記載。

FlaskアプリケーションのDocker化

こちらの記事がわかりやすかった

How to Dockerize a Flask Application
These days, developers need to develop, ship, and run applications quicker than ever. And fortunately, there's a tool th...

Dockerfile の作成

ローカル環境でアプリケーションのルートディレクトリに Dockerfile を作成する。 Dockerfile は、Flaskアプリケーションを実行するために必要な環境を定義。

Python
# 使用するPythonのベースイメージ
FROM python:3.9-slim

# 作業ディレクトリを設定
WORKDIR /app

# 依存関係ファイルをコンテナにコピー
COPY requirements.txt ./

# 依存関係のインストール
RUN pip install --no-cache-dir -r requirements.txt

# アプリケーションのコードをコピー
COPY . .

# アプリケーションを実行
# ホストOSからDockerコンテナ内のアプリケーションにアクセスするには、アプリケーションを 0.0.0.0 でリッスンさせる必要がある。0.0.0.0 はすべてのネットワークインターフェースでリッスンし、外部からのアクセスを受け付けるため、以下のように指定
CMD ["flask", "run", "--host=0.0.0.0"]

(備考)Dockerfileから作成されるDockerイメージの各レイヤに関して。上記Dockerfileの定義は、Dockerイメージの各レイヤを定義している。

Dockerイメージの本質 - Qiita
はじめに概要Dokcerにはストレージドライバという仕組みが存在します。このストレージドライバとは何か?どんな要素が含まれているのかを紐解くことで、Dockerイメージがどのように構築されてい…

requirements.txt

Zsh
flask
pillow

Dockerfileとrequirements.txtは、アプリのルートディレクトリに配置

Dockerイメージのビルドとテスト

Dockerfile があるディレクトリで、以下のコマンドを実行してDockerイメージをビルド。

これにより、アプリケーションがローカルのDocker環境で正しく動作することを確認できる。

Zsh
# -t オプションは「tag」の略で、ビルドされるイメージに名前(タグ)を付けるために使用される
# app-name はそのイメージに付ける名前です。この名前は任意で、後でイメージを識別しやすくするために使います。例えば、アプリケーションの名前やバージョンなど、意味のある名前を付けるのが一般的
# ドット . は、Dockerfileがあるディレクトリ。この場合、現在の作業ディレクトリ(コマンドを実行しているディレクトリ)。Dockerfile が別のディレクトリにある場合、そのディレクトリのパスを指定する必要あり。
docker build -t your-app-name .

# ビルドしたイメージを基にDockerコンテナの実行。ローカルのポート8080(localhost:5000)にアクセスすると、dockerコンテナの内部でポート5000で動いているアプリケーションに接続できるようになる。
docker run -p 8080:5000 your-app-name

# Dockerコンテナを実行する際に環境変数をセットする場合
docker run -p 8080:5000 -e OPENAI_API_KEY='your_openai_api_key_here' your-app-name

ポートマッピングに関して

Dockerのポートマッピングを図解すると以下のようになります。この例では、docker run -p 8080:5000 your-app-name コマンドの場合

    ┌─────────────────────────────────────────┐
    │               ホストマシン              │
    │                                         │
    │      ┌─────────────────────────────┐    │
    │      │         Dockerコンテナ        │    │
    │      │                             │    │
    │      │   ┌─────────────────────┐   │    │
    │      │   │ Flaskアプリケーション │   │    │
    │      │   │   (ポート5000)       │   │    │
    │      │   └──────────┬──────────┘   │    │
    │      │              │              │    │
    │      │              │              │    │
    │      └──────────────┼──────────────┘    │
    │                     │                   │
    │                     │                   │
    │     ┌───────────────┴───────────────┐   │
    │     │   ポートマッピング 8080:5000   │   │
    │     └───────────────────────────────┘   │
    │                                         │
    └─────────────────────────────────────────┘
  • Dockerコンテナ: この中にFlaskアプリケーションがあり、5000番ポートで待機している。
  • ポートマッピング 8080:5000: ホストマシンの8080番ポートからコンテナの5000番ポートにトラフィックが転送される。
  • ホストマシン: ここでDockerコンテナが実行されており、外部からのリクエストを受け取る。8080番ポートにアクセスすると、そのリクエストはコンテナの5000番ポートに転送され、Flaskアプリケーションに到達する。

ホストマシンの8080番ポートとコンテナの5000番ポートは、Dockerのポートマッピング機能によって連携している。このため、http://localhost:8080 にブラウザからアクセスすると、実際にはコンテナ内のFlaskアプリケーションにリクエストが届く。

Dockerのport mappingに関して詳しくはこちら

Zsh
~/dev/automate/mia-eye-test  docker run -p 8080:5000 mia-eye-test                                                                                    

 * Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on all addresses (0.0.0.0)
 * Running on http://127.0.0.1:5000
 * Running on http://172.17.0.2:5000
Press CTRL+C to quit
172.17.0.1 - - [09/Dec/2023 01:24:55] "GET / HTTP/1.1" 200 -
172.17.0.1 - - [09/Dec/2023 01:24:55] "GET /static/mia_face.png HTTP/1.1" 200 -
172.17.0.1 - - [09/Dec/2023 01:24:56] "GET /favicon.ico HTTP/1.1" 404 -
172.17.0.1 - - [09/Dec/2023 01:25:11] "GET / HTTP/1.1" 200 -
172.17.0.1 - - [09/Dec/2023 01:25:11] "GET /static/mia_face.png HTTP/1.1" 200 -

http://localhost:8080/でアクセスすると、無事画面が開いた。

Google Cloud Runにアプリケーションをデプロイ

Flaskアプリケーションをそのまま gcloud run deploy コマンドを使ってGoogle Cloud Runにデプロイすることはできない。Google Cloud Runはコンテナベースのサービスであり、アプリケーションはDockerコンテナとしてパッケージ化され、コンテナレジストリ(例:Artifact Registry)にアップロードされた後でなければデプロイできない。

Google CloudにCLIでログイン

Google Cloud SDK (gcloud コマンドラインツール) をインストールし、Google Cloudにログイン。

gcloud CLIのインストール(Mac)方法は、こちらの記事参照

ターミナルにgcloud CLI認証リンクが表示されるので、クリックしてgoogleアカウントでログインすると認証される。

Artifact RegistryでDockerリポジトリを作成

  1. Google Cloud Consoleにログインし、Artifact Registryページにアクセス。
  2. 「リポジトリの作成」を選択。
  3. リポジトリ名、リージョン、フォーマット(Docker)を指定。

Dockerイメージをローカルにビルドし、DockerでArtifact RegistryにPush

Dockerイメージをビルド

Zsh
# IMAGE_URLは、コンテナ イメージへの参照(us-docker.pkg.dev/cloudrun/container/hello:latest など)に置き換える。下記に詳細
docker build . --tag IMAGE_URL


# [REGION]、[PROJECT-ID]、[REPOSITORY-NAME]、[IMAGE]、[TAG]は適宜置き換える。
docker build -t [REGION]-docker.pkg.dev/[PROJECT-ID]/[REPOSITORY-NAME]/[IMAGE]:[TAG] .

# 今回の場合
# app: イメージの名前
# latest: イメージのタグ。ビルドのバージョンやリリースを区別するために使う。
docker build -t asia-northeast1-docker.pkg.dev/mia-linebot-dev/mia-eye-test/mia-eye-test-image:tag1 .

このままpushしようとしても

Zsh
denied: Permission "artifactregistry.repositories.uploadArtifacts" denied on resource "projects/mia-line-dev/locations/asia-northeast1/repositories/mia-eye-test" (or it may not exist)

とパーミッションのエラーが出る。

認証を構成

イメージを push または pull する前に、Google Cloud CLI を使用して Artifact Registry に対するリクエストを認証する。

クイックスタート: Docker コンテナ イメージを Artifact Registry に保存する  |  Artifact Registry documentation  |  Google Cloud
コンテナ イメージを保存するための非公開リポジトリを作成します。

ちなみに、Container Registryは非推奨になった。Google Cloud でコンテナの管理を始めるには、Artifact Registry を使用する。

IAM を使用したアクセス制御  |  Container Registry documentation  |  Google Cloud
Zsh

# リージョン`asia-northeast1`のDockerリポジトリに対する認証を設定
$ gcloud auth configure-docker asia-northeast1-docker.pkg.dev                                                                                                                                                                      

Adding credentials for: asia-northeast1-docker.pkg.dev
After update, the following will be written to your Docker config file located at [/Users/ky/.docker/config.json]:
 {
  "credHelpers": {
    "asia-northeast1-docker.pkg.dev": "gcloud"
  }
}

Do you want to continue (Y/n)?  y

Docker configuration file updated.

これによりDocker構成が更新される。Google CloudプロジェクトのArtifact Registryに接続して、イメージのpushとpullができるようになる。

ビルドしたイメージをArtifact RegistryにPush

Zsh
docker push [REGION]-docker.pkg.dev/[PROJECT-ID]/[REPOSITORY-NAME]/[IMAGE]:[TAG]

# 今回の場合
docker push asia-northeast1-docker.pkg.dev/mia-linebot-dev/mia-eye-test/mia-eye-test-image:tag1

無事うまくいくと、下記のようになる。

最初、認証もできたはずなのに、なぜかpushされなくて、よくよくみたらプロジェクトIDを間違えて記載していたorz

コンテナイメージをCloud Runにデプロイ

Cloud Run へのデプロイ  |  Cloud Run Documentation  |  Google Cloud

Cloud Runで新規にサービスを作成し、「既存のコンテナ イメージから 1 つのリビジョンをデプロイする」を選択する。選択ボタンを押すと、artifact registoryが開くので、作成したリポジトリ名を選択する。Artifact RegistryでプッシュしたイメージのURL。

CLIで行う場合

Zsh
gcloud run deploy [SERVICE-NAME] --image [REGION]-docker.pkg.dev/[PROJECT-ID]/[REPOSITORY-NAME]/[IMAGE]:[TAG] --platform managed --region [REGION]

[SERVICE-NAME] はCloud Runでのサービス名、[REGION][PROJECT-ID][REPOSITORY-NAME][IMAGE][TAG]はそれぞれ適宜置き換える。

環境変数の設定

環境変数を設定する場合は、こちらのドキュメント参照。

環境変数を使用する  |  Cloud Run Documentation  |  Google Cloud

少し動線が分かりにくいが、「コンテナの編集」画面で、下にスクロールして「変数とシークレット」というタブをクリックする。すると、環境変数設定画面が表示されるので、変数を追加する。

デプロイしたらエラー:port修正

デプロイしたところ、下記エラーが発生

Zsh
Default STARTUP TCP probe failed 1 time consecutively for container "mia-eye-test-image-1" on port 8080. The instance was not started.

port 8080 をリッスンしていないのでデプロイできませんというエラー。

Docker の場合との違い

  • Docker ローカル環境では、開発者が任意のポートを選択し、ポートマッピングを手動で設定することができる(例: p 8080:5000)。
  • しかしCloud Run ではこのようなマッピングは行われない。代わりに、Cloud Run はアプリケーションに PORT 環境変数を通じてポート番号を伝え、そのポートでのリッスンを期待する。

公式ドキュメント:https://cloud.google.com/run/docs/container-contract?hl=ja#port

app.pyを下記のように修正し、Dockerイメージを再ビルドし、Artifact Registry に再プッシュして、Cloud Run に再デプロイ。

Python
import os
from flask import Flask

app = Flask(__name__)

# 他のルート定義 ...

if __name__ == '__main__':
    # 環境変数 'PORT' からポート番号を取得。デフォルトは8080。
    port = int(os.environ.get('PORT', 8080))
    app.run(host='0.0.0.0', port=port)

今度は、無事成功!

めでたし、めでたし。

コメント

タイトルとURLをコピーしました