One-Stop Web Service Development using Docker and Cloud Run

この記事では GitHub, Docker そして Cloud Run を用いた Web アプリケーション開発と Deploy の方法を紹介します。

また本記事で紹介する構成はテンプレートを OSS として公開しています。
そのためすぐに開発を始めることができ、また簡単な手順で継続的に Deploy を続けられます。

今回ご紹介する構成と仕組みは特にデモンストレーションや PoC のような軽量プロジェクトを複数扱う場合に有効です。

Resources

この記事で扱うリソースは以下の GitHub template repository として公開済みです。

What’s Cloud Run?

はじめに「Cloud Run とは何か」「なぜ使うのか」を説明します。
一方、この記事では十分に有名な GitHubDocker の紹介は割愛します。

Cloud Run は Google Cloud Platform 上で提供されている、Web システムのための軽量な Docker コンテナのホスティングサービスです。
Cloud Run には”Fully Managed”と”Cloud Run for Anthos”の 2 種類がありますが、今回は Fully Managed のみを扱います。

Cloud Run を使えば次のようにごく簡単な手順で任意の Docker コンテナをあなたの GCP プロジェクト上に Deploy し、全世界に公開できます。
参考: https://cloud.google.com/sdk/gcloud/reference/run

gcloud run deploy hello-cloud-run\
 --image gcr.io/cloudrun/hello\
 --platform managed\
 --region us-central1

経験豊富なあなたは「コマンドラインや手順ではなくコードでインフラストラクチャを管理し Deploy したい」と考えるかもしれません。

有名な Infrastructure as Code(IaC) のツールである Terraform を使う場合、次のようなコードで前述の例と同等の Deploy を行えます。
(Example Usage - Cloud Run Service Basicより引用)

resource "google_cloud_run_service" "default" {
  name     = "cloudrun-srv"
  location = "us-central1"

  template {
    spec {
      containers {
        image = "gcr.io/cloudrun/hello"
      }
    }
  }

  traffic {
    percent         = 100
    latest_revision = true
  }
}

本記事でも Terraform の使用を想定します。

Cloud Run の特徴

Cloud Run の特長はなんといっても ”Container to production in seconds” のキャッチコピー通り、極めて簡単に Docker コンテナを公開できることです。
具体的には以下は利点でしょう。

一方で、Cloud Run には次の制約もあります。
もしこれらの制約が要件を満たさないのであれば他のサービスを組み合わせるべきでしょう。

  • データ永続化はできない
    • データの永続化には各種オブジェクトストレージやマネージド DBMS を併用します
  • ジョブ実行には不向き
  • Cloud Load Balancing には組み込めない
  • コンテナオーケストレーションは限定的
    • オーケストレーションが必要になったら Kubernetes 等を検討すべきでしょう
  • ライフサイクルやインスタンスタイプの細かいリソースチューニングは行えない
  • GPU は使用できない

総じて「オーケストレーションするほどではない」フェイズの Web システムに適したプラットフォームです。

もし他にも簡単に Docker コンテナをホストできるプラットフォームがあれば教えてください。

How to Develop Web Services for Docker Hosting Services?

Cloud Run を使えば Docker コンテナをそのままホストできることがわかりました。
では次に開発環境の Docker 化を考えます。

なぜ開発環境を Docker 化するのでしょうか。
開発と Deploy の環境差異は少なければ少ないほど無用な問題に遭遇せずに済みます。
例えば以下は代表的な遭遇したくない問題の例でしょう。

「開発中は動いていたのに Deploy したら動かない!」
「Deploy 環境での不具合を開発環境で Debug できない」
「前任者から引き継いだ手順がうまくいかない、あるいは煩雑すぎる」

開発から Deploy までを全て Docker 化することでこれらの問題の多くを回避できます。

GitHub Repositories

本記事の方法では Web システムの開発と Deploy を次の 2 つの GitHub repository で賄います。

  • Web Application Source Repository
    • Web アプリケーション自体のソースコードを管理する
  • Provisioning Repository
    • Provisioning 用のコードを管理する

冒頭に紹介したようにこれらは template repository として公開しています。

最初に誰かが template repository からプロジェクト用に GitHub repository を作れば、以降は次の手順で開発と Deploy を行えます。

  1. Git 管理外のファイルを受け取る
    • 例えば秘匿情報が書かれた .env file や credentials などです
  2. docker-compose up する
  3. docker-compose run xxx foo bar する

例えば以下のようなコマンドを実行することになるでしょう。

docker-compose run frontend npm run test
docker-compose run provisioning terraform plan

Web Application Source Repository

まず Web アプリケーションのソースコードを管理する GitHub repository について説明します。

これは次の template repository として公開されています。
https://github.com/mazgi/template.dockerized-nextjs-project

この template repository は当然ながら Web アプリケーション開発の雛形として作られているので、次の手順で Web アプリケーションがローカル実行できます。

1. .env file を作る

template repository の README から引用します。
開発環境として使っている Docker ホストに合わせて次の環境変数を設定します。

UID: Docker ホスト上の UID,
GID: Docker ホスト上の GID,
BIND_IP_ADDR: bind したい Docker ホスト上の IP アドレス, 例: 192.0.2.1
PUBLIC_IP_ADDR_OR_FQDN: 多くの場合 BIND_IP_ADDR と同じ(後述)

2) docker-compose up

template では次の Docker Compose services が Docker ホストのネットワークに接続されて起動します。

  • Web Frontend: $BIND_IP_ADDR:3000
  • Web API(BFF): $BIND_IP_ADDR:4000
  • MySQL: $BIND_IP_ADDR:3306
  • Redis: $BIND_IP_ADDR:6379

つまり、Web ブラウザで http://$BIND_IP_ADDR:3000 にアクセスすれば Frontend が表示され、
http://$BIND_IP_ADDR:4000 にアクセスすれば Web API が表示されます。

また MySQL クライアントを用いて mysql -h \$BIND_IP_ADDR -u root と実行することで Docker コンテナとして実行されている MySQL サーバーに接続することもできます。

補足: 開発サーバーの IP アドレスとポート

開発用の各種サーバーがどのように Docker ホストのネットワークに bind されるかは .env file の BIND_IP_ADDR 変数で指定します。
一方でポートは docker-compose.yml 上で定義しており、プロジェクトごとに変えることは想定していません。

bind する IP アドレスは一般的に以下の方法で複数確保できます。

  • IP aliasing
  • VirtualBox 等で VM を起動する
  • Google Compute Engine 等で VM を起動する

IP alias は次のようなコマンドで設定できます。

macOS:

sudo ifconfig en0 alias 192.0.2.1 255.255.255.0`

Linux(ifconfig command):

sudo ifconfig eth0:1 192.0.2.1/24

Linux(ip command):

sudo ip addr add 192.0.2.1/24 dev eth0 label eth0:1

同じ template repository から作成したプロジェクト ab を同時に開発するためには、IP アドレスかポート、少なくともいずれかを重複しないように設定する必要があります。

IP アドレスは最小 1 つしか割り当てられない一方、ポート番号は 65000 個ほど選べるため、プロジェクト ab で IP アドレスではなくポート番号を変更したいと思うかもしれません。
しかし、次の理由でポート番号は固定しておく方が利便性が高いと考えています。

例えば MySQL には 3306、Redis には 6379 といった標準的なポート番号があります。
ポート番号を標準から変更しなければツール使用時にオプションを指定する必要もありませんし、プロジェクトを移った際に「このプロジェクトの mysql port は 3306 だったか、それとも 13306 だったか」と確認する必要もありません。

またプロジェクトごとにポート番号オフセットして回ることはわかりづらく、私にような忘れっぽい人間はポート番号の変更や重複に気付かず発生したトラブルに時間を費やさずに済みます。
チーム開発する上で IP アドレスだけ教えてもらえば他の人の環境でもエンドポイントがわかることは便利です。

補足: BIND_IP_ADDR と PUBLIC_IP_ADDR_OR_FQDN が異なる場合

大抵の場合、.env file の PUBLIC_IP_ADDR_OR_FQDN には BIND_IP_ADDR と同じローカル IP アドレス、例えば 192.0.2.1 などを指定すれば問題ありません。
しかしクラウドサービス上の VM のように NAT 経由でアクセスする場合は適した IP アドレスを指定する必要があります。

例えばあなたのインスタンスのローカル IP アドレスが 192.0.2.1 で NAT で割り当てられたグローバル IP アドレスが 203.0.113.1 であるなら、これらを .env file に書く必要があります。

補足: プロジェクト内にサービスを追加する方法

もし Docker Compose で起動される新たなサービス $SERVICE を追加する場合は以下が必要です。

  1. GitHub repository 直下にソースコードディレクトリ $SERVICE/ を作る
  2. 開発用 Dockerfile Dockerfile.d/$SERVICE.development/Dockerfile を作る
  3. Deploy 用 Dockerfile Dockerfile.d/$SERVICE/Dockerfile を作る
  4. docker-compose.yml を編集して $SERVICE を追加する
  5. .github/workflows/*.yml を編集して GitHub Actions で $SERVICE の CI/CD を設定する

ただし Deploy 用の Dockerfile は現在のところ template repository では未定義です。

Provisioning Repository

次に Provisioning 用のコードを管理する GitHub repository について説明します。
これは次の template repository として公開されています。
https://github.com/mazgi/template.dockerized-provisioning-project

この GitHub repository では主に Terraform のコードを管理します。

template repository には以下を行う tf ファイルが provisioning/examples にサンプルとして配置されています。

  • Terraform 自体の設定
    • 各種 Terraform provider のバージョン指定
    • GCS に tfstate ファイルを格納
  • GitHub Actions 向けのシステムユーザー作成
  • Cloud DNS による DNS ゾーン設定
  • Amazon SES の設定

これらのサンプルを必要に応じて 1 階層上の provisioning に移動することで適用の対象となります。

初期構築の例

Template repository を使用して GitHub repository を作った後は次の手順で provisioning を開始でき、必要なら GitHub Actions 上で自動適用も行えます。

  1. credentials と .env file 配置
  2. provisioning/examples/* から必要なファイルを provisioning/ にコピー
  3. docker-compose up
    • ステータスが 0 で返って終了すれば正常です
  4. 一度 local で apply
  5. GitHub Actions 向けの credentials が払い出されるので GitHub repository の secrets に登録

以降は main branch に commit が push されると GitHub Actions で terraform planterraform apply が行われます。

運用方法の例

前述の初期構築が完了した GitHub repository であれば次の 3 ステップで provisioning を行えます。

  1. credentials と .env file を受け取る
  2. docker-compose up して正常終了を待つ
  3. docker-compose run provisioning terraform apply を実行する

補足: Init script

docker-compose up 時に実行される初期化スクリプト内で次のことを行なっています。
https://github.com/mazgi/template.dockerized-provisioning-project/blob/main/scripts/provisioning.init-with-google.sh

  1. GCP service account の認証
  2. GCS bucket の作成
  3. terraform init 実行

Terraform 実行を Docker 上で行う理由

Terraform はとてもポータビリティに優れたツールで、Linux, macOS や Windows 向けにもシングルバイナリで提供されています。
一見、Docker 上で実行するメリットは少ないと感じるかもしれません。

しかし Terraform 単体ではなく「provisioning を行う環境」として考えると Docker に閉じることにはメリットがあります。

1 つは秘匿情報が .env file と credentilas に閉じることです。

プロジェクト内の gitignore された特定の path に credentials を配置するだけですぐに目的の環境に向けて provisioning できますし、誤って異なる環境に向けて provisioning してしまうリスクを軽減できます。

きっと多くの方が gcloud config set project し忘れたり思わぬ環境変数が export されていて冷や汗をかいた経験があるのではないでしょうか。

もう 1 つのメリットは環境差異の吸収です。

環境やバージョンによる差異はシェルスクリプトですら深刻な影響をもたらすことがあります。
例えば Linux と macOS での Bash バージョンの違いや、jq などある人にとっては「インストールされていることが当たり前」なツールの有無による差異で悩んだことは誰しもあるのではないでしょうか。

実際のところ、プロジェクトに新しく参加した方の sedawk が BSD 版/GNU 版であることに気付かず消費された時間は通勤時間の次に勿体無いと思っています。

provisioning を行う環境すら Docker に隠蔽することでこれらの問題を大きく軽減することができます。

これらのメリットは GitHub Actions のような CI/CD を設定する上でも有用です。
実際のところ、 .env file を書き出すように設定し credentials を secrets に登録するだけで、すぐに CI/CD を開始できます。

その他補足事項とまとめ

本項ではここまでの内容から漏れたいくつかの補足を記載します。

IDE/エディタは Visual Studio Code を想定

私は近年 Visual Studio Code(VSCode) を好んで使っています。
特に JavaScript や TypeScript で開発する際は prettier と eslint を VSCode の extensions から参照できるよう考慮しています。

しかし template repository は特に VSCode でなければ使えないような構成にはなっていません。
お好きな IDE やエディタを使って開発できます。

また template repository は Visual Studio Code Remote - Containers 向けには構成されていません。

Visual Studio Code Remote - Containers は開発を容易にする優れた機能ですが、 1 つの Docker コンテナにしか接続できません。
そのため本記事で紹介する用途と構成には向いていません。

Docker コンテナは開発用と Deploy 用で分ける

開発用 Docker コンテナは使いやすさを重視しサイズが肥大化することをある程度許容しています。
私が実際に使っている開発用の Dockerfile を次の GitHub repository に集約されており、GitHub Actions で自動ビルドされ ghcr.io で公開されています。
https://github.com/mazgi/dockerfiles

Deploy 用の Dockerfile では Alpine 等の軽量な Linux ディストリビュージョンをベースイメージとして使うことが多いですが現時点では template repository には組み込めていません。

Docker 単体ではなく Docker Compose を使う

Docker コマンド、覚えてますか?
私が覚えているコマンドは docker run -it -p --rm -v -w 程度です。

Docker コマンドの全てを覚えなくても docker-compose.yml に 1 度書いておけば up, run, down 程度を使い分ければ済みます。

2020 年の Web 開発用 OS は Linux こそ至高

Docker はその仕組み上、ホスト OS とコンテナで Linux kernel を共用します。
これは Docker に限った話ではなく LXC 等のコンテナ技術全般に言える話です。

そのため Docker の Linux 向けコンテナを macOS や Windows 上で動かす際には、macOS や Windows 上で Linux VM を起動させ、その Linux VM 上で Docker コンテナを動作させることになります。
GUI などで使い勝手よく工夫されてはいますが、この仕組み自体は変わりません。

そのため、Docker コンテナのファイル I/O は macOS や Windows 上では非常に性能が落ちます。
この性能低下は前述のように Docker とコンテナ自体の構造に根ざしているため、当面は大きく改善しないでしょう。
ネットワーク構成やコストが許せば Linux がインストールされた PC や Goole Compute Engine のような VM を用意することで快適に開発を進められます。

冒頭に書いたように本記事では、デモンストレーションや PoC のような軽量な複数の Web アプリケーションプロジェクトを、いかに継続的に容易に扱うかを考え試した内容をご紹介したものです。
もし本記事の内容や template repository が私と同じ悩みを抱えたどなたかのお役に立てば幸いです。