Google Cloud Run で Clojure アプリケーションを実行しよう

              · · ·

この記事は、 Clojure Advent Calendar 2019 の 12日目 の記事です。

はじめに

新しい言語を学ぶにあたってモチベーションを保って学習を継続して行くために、自分用のちょっとしたツールを実装してみる、というのはみなさん良くやることかと思います。 私個人はClojure に入門して半年ほどですが、この素敵な言語を使って簡単なサービスを作ってみようかと思ったときに、せっかくだから手持ちのスマホからも使いたいと思う様になりました。しかし、自分個人用のサービスに対して EC2 や GCE のインスタンスを管理するのは、手間もコストもかかってしまいます。

Cognitect, inc. が AWS を推していることもあってか、Clojure のアプリケーションをクラウド環境で稼働させるとなった場合、AWS のサービスを選択している記事を見かけることが多い気がします。私もお仕事では AWS のサービスを使うことが多いですが、個人での開発では GCP を使っています。 そこで、GCP で展開されているサービス(プロダクト)の中で、要件にマッチするものが何かを考えていました。

そんな中、Cloud Run (フルマネージド型) が GA を迎えたというニュースの中で、サーバーレス オプションの選択 | Google Cloud にたどり着きました。当該ページには、 GCP 上のサーバレスコンピューティングプラットフォーム の中で何を選ぶべきかの意思決定表があります。(以下に転載)

serverless-guide

上記を見るに、Clojure にて実装した WEBアプリケーション は Cloud Run で実行するのが良さそうです。1

Cloud Run とは?

Cloud Run は、フルマネージドなコンピューティング プラットフォームで、HTTP リクエスト経由で呼び出すことができる ステートレスなアプリケーションコンテナを実行できます2。なお、フルマネージド型のコンテナ管理の他に、Cloud Run with GKE を使用して Google Kubernetes Engine クラスタ内でコンテナを実行することも可能とのことですが、今回は触れていません。

コンテナ化した Clojure アプリケーションを実行する という点で言えば Google App Engine のフレキシブル環境(FE) があります。 しかし、App Engine FE は 待機時にも常に 1つ のインスタンスが稼働してしまうため、運用コストの面でちょっと辛いです。(無料枠も無いし) 一方、Cloud Run は待機中のインスタンス数を ゼロ にすることができ、さらに 無料枠 も適用されるため、 今回の様に 想定しているクライアントの少ない(というか私だけ)であるアプリケーションにはぴったりかと思います。

注意

GCP プロジェクト内で Cloud Run API を有効にするためには、課金を有効にする必要があります。 前述の通り、Cloud Run は無料枠が用意されているプロダクトですが、その枠を超える負荷をかければ当然課金させます。 もし、この記事を読んで ご自身で試してみようと思う方はその点ご留意ください3

また、Cloud Run 自体はまだ ベータ版 のプロダクトです。以下、記載されている手順などは陳腐化する可能性ががありますので、タイミングによっては公式ドキュメントをあたる様にしてください。

事前準備

早速 Cloud Run を試してみるにあたって、クライアントツール Cloud SDK や テスト用の GCPプロジェクト を準備して行きます。

Google Cloud SDK をインストールする:

もし Cloud SDK の導入がまだの場合は、 Google Cloud SDK のドキュメント にしたがってダウンロード&インストールしてみてください。

# インストール後の確認
$ gcloud --version
Google Cloud SDK 271.0.0
...

前述した通り、Cloud Run はプレリリース版であるため、デフォルトでは Cloud SDK にて 管理できません。 以下を実行して 別途 当該コンポーネントをインストールしましょう。

gcloud componenst install beta
$ gcloud components install beta


Your current Cloud SDK version is: 271.0.0
Installing components from version: 271.0.0

┌─────────────────────────────────────────────┐
│     These components will be installed.     │
├──────────────────────┬────────────┬─────────┤
│         Name         │  Version   │   Size  │
├──────────────────────┼────────────┼─────────┤
│ gcloud Beta Commands │ 2019.05.17 │ < 1 MiB │
└──────────────────────┴────────────┴─────────┘

For the latest full release notes, please visit:
  https://cloud.google.com/sdk/release_notes

Do you want to continue (Y/n)?  Y # <=== Y を入力
 
╔════════════════════════════════════════════════════════════╗
╠═ Creating update staging area                             ═╣
╠════════════════════════════════════════════════════════════╣
╠═ Installing: gcloud Beta Commands                         ═╣
╠════════════════════════════════════════════════════════════╣
╠═ Creating backup and activating new installation          ═╣
╚════════════════════════════════════════════════════════════╝

Performing post processing steps...done.

Update done!

これで Google Cloud SDK にて Cloud Run が扱える様になりました。

Google Cloud プロジェクトを作成する:

すでに作成済みのプロジェクトに Cloud Run を導入する場合には不要ですが、 今回は お試し用に 新規プロジェクトを作成します。以下を実行し、プロジェクトを新規作成しましょう。

gcloud projects create --name hello-cloud-run --set-as-default

--name で指定している プロジェクト名称 はお好きなもので問題ありません。 また、 {project-id} を 指定せずに作成しようとした場合はランダムで払い出されたものを使用するか訊ねられます。 今回はお試し用なのでそのまま採用しました。

$ gcloud projects create --name hello-cloud-run --set-as-default
No project id provided.

Use [hello-cloud-run-261706] as project id (Y/n)?  Y  # 入力

課金を有効にする:

Cloud Run を使用するためには、プロジェクトに対する課金を有効にする必要があります。 プロジェクトダッシュボード にて お支払い請求アカウントとの紐付け を実施することで、課金が有効化されます。

GCP_HOME

GCP_Payment

Cloud Build API と Cloud Run API を有効にする:

今回は、Cloud Build を経由して Cloud Run へのアプリケーションのデプロイを行います。 そのため、Cloud Run API だけでなく、 Cloud Build API も有効にしておきました。 APIの有効化は、コンソール にて実行することができます。

GCP_Enable_CloudBuildAPI

GCP_Enable_CloudRunAPI.jpg

この時点で、 Cloud Run 環境へアプリケーションをデプロイする準備が整いました。 なお、前段の 課金の有効化 を未実施の場合は、各種 API が有効にできません。

デプロイするアプリケーションを実装する

ここでやっと Clojure が登場します。 デプロイするアプリケーションを実装して行きましょう。

アプリケーションが満たすべき制限

コンテナ契約 に記載されているとおり、 Cloud Run にて稼働させるアプリケーションは、 環境変数 PORT にて指定された ポート番号をリッスンする必要があります。

以下の通り、実際は 8080 ポート決め打ちでも問題は無いようです。 が、大人しく忠告にしたがって環境変数から値を受け取る様に実装しましょう。

In Cloud Run container instances, the PORT environment variable is always set to 8080, but for portability reasons, your code should not hardcode this value.

実装内容

今回はおそらくみなさんおなじみの ring を使い、 URLパラメータで受け取った name に対して挨拶を返す簡単な WEB API を実装しました。

deps.edn
src/greeter.clj
resources/log4j.properties
;;; deps.edn
{:paths ["resources" "src"]
 :deps {org.clojure/clojure {:mvn/version "1.10.1"}
        log4j/log4j {:mvn/version "1.2.17"}
        org.clojure/tools.logging {:mvn/version "0.5.0"}
        ring/ring-core {:mvn/version "1.8.0"}
        ring/ring-jetty-adapter {:mvn/version "1.8.0"}}
 :aliases
 {:uberjar
  {:extra-deps {uberdeps {:mvn/version "0.1.6"}}
   :main-opts ["-m" "uberdeps.uberjar"
               "--target" "target/app.jar"]}}
 :mvn/repos
 {"central" {:url "https://repo1.maven.org/maven2/"}
  "clojars" {:url "https://repo.clojars.org/"}}}
;;; src/greeter.clj
(ns greeter
  (:gen-class)
  (:require [ring.adapter.jetty :as s]
            [ring.middleware.keyword-params :refer [wrap-keyword-params]]
            [ring.middleware.params :refer [wrap-params]]
            [clojure.tools.logging :as log]))

(defn handler [req]
  (log/info ::handle req)
  (let [name (or (get-in req [:params :name])
                 "World")]
    {:status 200
     :headers {"Content-Type" "text/html"}
     :body (format "Hello, %s !!" name)}))

(defn -main [& args]
  (-> handler
      wrap-keyword-params
      wrap-params
      (s/run-jetty {:port (if-let [p (System/getenv "PORT")]
                            (Integer/parseInt p)
                            8080)
                    :join? false})))
# resources/log4j.properties
log4j.rootLogger=INFO, console
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=[%p] %m%n

ローカルで実行してみる

まずは スタンドアローン な jar を作成してみましょう。 deps.ednaliases: uberjar にて指定しているとおり、 今回は uberdeps を使っています。

clj -A:uberjar
$ clj -A:uberjar
[uberdeps] Packaging target/app.jar...
  ...
  ...
[uberdeps] Packaged target/app.jar in 1370 ms

生成された app.jar を実行し、アプリケーションを起動します。 環境変数からポート番号が指定できることを確認するために、あえて 8080 以外を指定してみます。

env PORT=9000 java -cp target/app.jar clojure.main -m greeter
$ env PORT=9000 java -cp target/app.jar clojure.main -m greeter
...
2019-12-11 23:45:23.250:INFO:oejs.AbstractConnector:main: Started ServerConnector@48ea2003{HTTP/1.1,[http/1.1]}{0.0.0.0:9000}
2019-12-11 23:45:23.251:INFO:oejs.Server:main: Started @2396ms

疎通を確認してみます。

curl localhost:9000/?name=micheam
$ curl localhost:9000/?name=micheam
Hello, micheam !!

ちゃんと指定した名前に挨拶が返ってきました。 ローカルでの動作は大丈夫そうです 😌

Dockerfile を作成する

ルートディレクトリにて、Dockerfile を作成しましょう。 Getting Started で紹介されている Dockerfile と同様に マルチステージ ビルド構成にて 記述してみました。

#######################
# BUILD STAGE
#######################
FROM clojure:openjdk-11-tools-deps-slim-buster as builder

WORKDIR /app
COPY deps.edn .
COPY src ./src
COPY resources ./resources

RUN clj -A:uberjar

#######################
# EXECUTION STAGE
#######################
FROM openjdk:11-jre-slim

COPY --from=builder /app/target/app.jar /app.jar
CMD ["java", "-cp", "/app.jar", "clojure.main", "-m", "greeter"]

Docker イメージのビルドを確認

作成した Dockerfile を使って、ローカルで Docker イメージを作成してみましょう。 なお、初回のビルドでは 依存ライブラリを大量にダウンロードする必要があるため、 それなりの実行時間がかかると思います。一方、2回目以降の Docker イメージのビルドでは、 ローカルリポジトリが再利用されるため、差分のみのダウンロードとなり、高速になります。 これは、 Cloud Run へのデプロイ時も同様の動きとなります。

docker build . -t greeter
# 2回目の場合
$ docker build . -t greeter
Sending build context to Docker daemon  7.564MB
Step 1/9 : FROM clojure:openjdk-11-tools-deps-slim-buster as builder
 ---> f74d71b47c79
Step 2/9 : WORKDIR /app
 ---> Using cache
 ---> fe13e1431e54
Step 3/9 : COPY deps.edn .
 ---> Using cache
 ---> 19e9f8005dcf
Step 4/9 : COPY src ./src
 ---> Using cache
 ---> 494d55f51ff5
Step 5/9 : COPY resources ./resources
 ---> Using cache
 ---> 1ccba53c2a24
Step 6/9 : RUN clj -A:uberjar
 ---> Using cache  # <========== キャッシュが使用される
 ---> c3fddbb203ba
Step 7/9 : FROM openjdk:11-jre-slim
 ---> 0e452dba629c
Step 8/9 : COPY --from=builder /app/target/app.jar /app.jar
 ---> 4db2c6903e16
Step 9/9 : CMD ["java", "-cp", "app.jar", "clojure.main", "-m", "greeter"]
 ---> Running in 261ce51d3666
Removing intermediate container 261ce51d3666
 ---> b40859abf53c
Successfully built b40859abf53c
Successfully tagged greeter:latest

作成された Docker イメージ を指定し、コンテナを起動してみます。

docker run -p 18080:8080 --rm {image_id}
$ docker run -p 18080:8080 --rm b40859abf53c
2019-12-11 15:19:41.887:INFO::main: Logging initialized @2686ms to org.eclipse.jetty.util.log.StdErrLog
2019-12-11 15:19:42.221:INFO:oejs.Server:main: jetty-9.4.22.v20191022; built: 2019-10-22T13:37:13.455Z; git: b1e6b55512e008f7fbdf1cbea4ff8a6446d1073b; jvm 11.0.5+10
2019-12-11 15:19:42.304:INFO:oejs.AbstractConnector:main: Started ServerConnector@c6b2dd9{HTTP/1.1,[http/1.1]}{0.0.0.0:8080}
2019-12-11 15:19:42.309:INFO:oejs.Server:main: Started @3108ms

疎通はできるかな?

$ curl localhost:18080
Hello, World !!

コンテナ内で起動した アプリケーション に対しても疎通の確認ができました。

コンテナイメージ を Google Container Registry に登録

ここまでの手順で、(1) 使用するGCPプロジェクト (2) 配備するアプリケーション (3) Dockerfile の3点が用意できました。Cloud Run へコンテナをデプロイ 可能にするために、資材を Cloud Build に登録しつつ、そこから Dockerイメージ のビルドおよび Google Container Registry への登録を行います。

gcloud builds submit --tag gcr.io/{project-id}/{image-name}
$ gcloud builds submit --tag gcr.io/hello-cloud-run-261706/greeter
Creating temporary tarball archive of 17 file(s) totalling 99.7 KiB before compression.
Uploading tarball of [.] to [gs://hello-cloud-run-261706_cloudbuild/source/1576079509.23-faf7750e36274adba2ef1e7d48080b9a.tgz]
Created [https://cloudbuild.googleapis.com/v1/projects/hello-cloud-run-261706/builds/4dc3ef29-bd0d-44d4-8501-2b0495274d54].
Logs are available at [https://console.cloud.google.com/gcr/builds/4dc3ef29-bd0d-44d4-8501-2b0495274d54?project=1007829969744].

メッセージにも記載されているとおり、ビルド結果は コンソール でも確認することができます。

GCP_Enable_CloudBuild_Result.jpg

また、ビルドされたコンテナイメージングが Container Registry に登録されたことも確認できます。 GCP_Container_Registry.jpg

Cloud Run へのデプロイ

Container Registry に登録したコンテナイメージを Cloud Run へデプロイします。Cloud Run では、スケーリングは自動に行われます。商用サービスに適用する際には、想定される負荷に応じて最大インスタンス数を設計するべきです。しかし、今回は個人開発したサービスを稼働させることを想定しているため、自動スケーリングによる予期せぬ請求増加を防ぐために、max-instances1 を指定します。(デフォルト 1000

gcloud run deploy --image gcr.io/{project-id}/{image-name} --platform managed --max-instances 1
$ gcloud run deploy --image gcr.io/hello-cloud-run-261706/greeter --platform managed --max-instances 1
Please specify a region:
 [1] asia-northeast1
 [2] europe-west1
 [3] us-central1
 [4] us-east1
 [5] cancel
Please enter your numeric choice:  1 # リージョンは, asia-northeast1

To make this the default region, run `gcloud config set run/region asia-northeast1`.

Service name (greeter):
Allow unauthenticated invocations to [greeter] (y/N)?  y # 認証なしでの実行を可能に

Deploying container to Cloud Run service [greeter] in project [hello-cloud-run-261706] region [asia-northeast1]
✓ Deploying new service... Done.
  ✓ Creating Revision...
  ✓ Routing traffic...
  ✓ Setting IAM Policy...
Done.
Service [greeter] revision [greeter-00001-guh] has been deployed and is serving 100 percent of traffic at https://greeter-6qs3k6nphq-an.a.run.app

特に問題なく デプロイ が完了しました。メッセージには、全てのトラフィックは https://greeter-6qs3k6nphq-an.a.run.app で受けているよ。とあるので、早速叩いてみましょう

Service [greeter] revision [greeter-00001-guh] has been deployed and is serving 100 percent of traffic at https://greeter-6qs3k6nphq-an.a.run.app

$ time curl https://greeter-6qs3k6nphq-an.a.run.app
Hello, World !!
real	0m0.742s
user	0m0.025s
sys	0m0.024s

しっかりと結果が返ってきました。

$ time curl https://greeter-6qs3k6nphq-an.a.run.app?name=micheam
Hello, micheam !!
real	0m0.148s
user	0m0.022s
sys	0m0.016s

パラメータもしっかり受け取れているので、無事 Cloud Run へのアプリケーションの配備が完了しました 🎉

おわりに

少々長くなってしまいましたが、Clojure で実装したアプリケーションを Cloud Run で実行するまでの流れが伝わったでしょうか。Cloud Run は、実行するアプリケーションに対して 任意のレジストリに登録されたコンテナイメージである という点しか要求しないため、とても導入しやすいサービスでは無いかと思います。気が変わって他のクラウドサービスに移行したくなったとしても、資材に手を入れる必要が無いのは素敵なことですね。

待機中のインスタンス数を ゼロ にでき、かつ 無料枠が存在する という点を考えると、趣味の範囲であったりスモールビジネス向けのアプリケーション稼働環境としてもなかなかに優秀なのではないかと思います。

Clojure Advent Calendar の記事と言いつつ、Clojure に関する情報はおまけ程度になってしまった気もしますが、何かのお役にたてば幸いです 😅

おわり


  1. App Engine フレキシブル環境 でも可能でしょうが、お値段的にちょっと… [return]
  2. 何かの呪文かな?😅 [return]
  3. まあ、自己啓発にもある程度の投資は必要ですよね? [return]

comments powered by Disqus