DockerのイメージをGAE/FEにデプロイする


Colorful Boardのwasnotです。
社内では最近、積極的にGCPを活用していますが、今回はその中でもGAE/FEでのDockerデプロイについて、python環境での環境構築の例を紹介したいと思います。

※この記事はColorful Board Advent Calendar 2016の記事です。
また、元々社内のQiita:Team上で共有していた記事を加筆、修正しています。

はじめに

GAE/FE(Google App Engine / Flexible Environment)はGAEのような感覚でGCEインスタンスを管理したり、オートスケールなどを行うことができるサービスです(だと思っています)。

標準の環境でpython2/3をはじめ、Go/Node/Rubyなど色々と用意されていますが、今回はChainerなどのライブラリが必要だったのでカスタムランタイムを使います。
また、将来GKEなどでのスケーリングを検討したいので、Dockerイメージを使ったデプロイをしたいと思います。

デプロイや環境コピーが簡単なので試してみました。
gcloudコマンドがインストール済みなことが前提です。

起動するアプリを書く

まずはサーバとしてリクエストを受け取るpythonのWSGIアプリを書きます。
一番シンプルな、bottleの例を書きます。

app.py
from bottle import Bottle
app = Bottle()

@app.route(['/hello', '/'])
def hello():
    return "Hello World!"

@app.route("/_ah/health")
def _ah_health():
    return "ok"

if __name__ == '__main__':
    # waiting request
    app.run(host='0.0.0.0', port=8080, debug=True)

GAE/FEにDockerイメージアプリをデプロイする場合は、GCEインスタンス上にDockerのイメージを立ち上げてアプリを起動できるようにすればいいだけです。
ポーティングなどはGAEのように自動で設定してくれます。

注意点

詳しい注意点はこちらに書いてありますが、少しかいつまんで見ます。
Building Custom Runtimes

ポートは8080

アプリでポーティングされるのは8080ポートで固定のようです。
上のようにlocalhost:8080とかをリッスンするようにしましょう。

health checkの対応

追記:下記の従来型のHealth checkは2018/9/30に終了するそうです。
新しいhealth checkはこちらを参照してください。

GAE側がちゃんとアプリが起動しているかを確認するために、コンテナ起動後にhealth checkを行なってきます。
/_ah/healthへのリクエストに200 OKを返せるように実装してください。

app.yamlで以下の設定をすれば不要なはずですが、試した時はうまくいかず、デプロイが中断されてしまいました。(たまたまかもしれません)

app.yaml
health_check:
  enable_health_check: True

また、起動時にあまり時間が掛かる処理を行ったりするとtimeoutになってしまうのでスペックを上げて高速化するか後回しor非同期にするかしたほうがいいと思います。

そのほか、/_ah/start/_ah/stopにもリクエストがくるので、起動・停止を取得することができそうです。

app.yamlやDockerfileを作成

Dockerイメージのデプロイには、app.yamlやDockerfileが必要ですが、普通の形式だとうまくgcloudツールが認識してくれなかったりしたので、gcloudのジェネレータを使った方が早そうでした。
ソースコードがあるディレクトリで

$ gcloud beta app gen-config

とやるとスムーズでした。
pythonアプリケーションかどうかを質問されました。
元にするコンテナイメージが違うようです。

FROMするイメージ

自分でDockerfileを書く場合でもFROMするイメージはgcr.io/google_appengine以下のレポジトリのイメージが良さそうです。
具体的には以下の中から選ぶといいようです。

Configuring the Dockerfile

Runtime dockerのFROMコマンド
Go FROM gcr.io/google_appengine/golang
Java FROM gcr.io/google_appengine/openjdk
Java+Jetty FROM gcr.io/google_appengine/jetty
Python FROM gcr.io/google_appengine/python
Node.js FROM gcr.io/google_appengine/nodejs
Ruby FROM gcr.io/google_appengine/ruby

生成内容を修正

以下に自動生成されたものを加筆した例を書いておきます。

app.yaml
# 自動生成部分
entrypoint: python app.py
runtime: custom
vm: true
# もしモジュール(サービス)を分割する場合は指定しておく
service: flex

# オートスケーリングだとデフォルト2台待機なので少なくしたりする
automatic_scaling:
  min_num_instances: 1
  max_num_instances: 20

# マシンタイプはデフォルトミニマムなので適宜指定。これは下から二つ目。
resources:
  cpu: .5
  memory_gb: 3
FROM gcr.io/google_appengine/python:1.3
# ここではpython2.7が指定されています。
RUN virtualenv /env -p python

# Set virtualenv environment variables. This is equivalent to running
# source /env/bin/activate
ENV VIRTUAL_ENV /env
ENV PATH /env/bin:$PATH
ADD . /app/
# こんなのを加えるとスムーズに環境構築されると思います。
RUN pip install -r requirements.txt
CMD python app.py

app.yamlは通常のGAEと同じような感覚でresourcesなどを指定していけばいいと思います。
あまりいじれる部分はなかったと思います。
endpointの指定はDockerのCMDの指定があれば、なくても動いたかもしれません。(忘れました)

Dockerfileについてはpython2.7の例を書いています。
元のイメージが3.5と2.7どちらも入っているのでvenvで切り替える感じです。
あとは普通のDockerアプリと同じでしょうか。

デプロイ

appcfg.pyコマンドではデプロイできないのでgcloudコマンドを使います。

$ gcloud beta app deploy --project my-project --version flex

appcfgコマンドだとapp.yamlに書けば認識されていた情報であるprojectやversionなどが、beta版のためか、読み取れないと言われたので、指定しています。
最近はgcloudツールに統合の動きが見られる?のでこちらが標準になるかもしれませんね。

意外と時間がかかる

デプロイは元のイメージからpullしたり、イメージのキャッシュ?をGCS上にアップしたりその差分を確認したりと、何かと時間がかかる印象です。
5-15分くらいは平気でかかります。
しかもhealth checkが読めなかったり、アプリがエラーで起動できなかったりして中断されることもしばしば。。
特に、なんでもないのにエラーで失敗することもたまーにある(リトライでデプロイ成功)ので、おおらかな気持ちの時にデプロイするのがいいと思います。

停止

起動したあとは普通にGAEアプリのようにリクエストの数などがダッシュボードに出ますが、課金は普通のGCEインスタンスとしてもされます。
起動している時間分は課金されてしまうので、不要になったら停止した方がいいでしょう。

停止する場合はGCEのインスタンスから止めても自動で復活させられてしまうので、
AppEngineのバージョンから該当のバージョンを停止・削除等するほうがいいです。

まとめ

自分はあまりDockerをいじったことがなかったので、Docker入門としては割と面白くて簡単かなぁ、と思います。
GKEを使ったことがないので、GKEとの差分はイマイチわかっていませんが、GKEの方が複数リージョンで統合管理できたり、一インスタンスに複数コンテナを起動できたりと高機能な印象を持っています。
ただ、簡単にアプリを起動してGAEのようにインスタンスもすぐに複製したりDatastore等に簡単にアクセスできたりする利点も捨てがたいかなぁと思うので、インスタンスのスケールの規模とかに応じて使い分けるのがいいのではないでしょうか。

次回は実際に使っているDockerのイメージについて紹介したいと思います。