めもめも

このブログに記載の内容は個人の見解であり、必ずしも所属組織の立場、戦略、意見を代表するものではありません。

Google Container Engineで五目並べアプリのAPIサーバーを作るデモ

何の話かというと

Dockerコンテナでアプリを作ると便利ですが、何でもかんでもコンテナに突っ込むと(たとえば、RDBとか)面倒な事も多くなります。

・スケーラビリティが必要
・機能単位のリファクタリング/アップデート(マイクロサービス化)が必要

という部分にフォーカスしてコンテナ化して、その他のパーツは、コンテナ以外の環境と組み合わせた方が幸せになれるかも知れません。

というわけで、GCPを利用して、こんな感じのアプリケーション環境を構築するデモの手順を紹介します。

https://github.com/GoogleCloudPlatform/gke-gobang-app-example/raw/master/docs/img/architecture.png

・五目並べゲームのAPIサーバーを作ります。
・ゲームの進行を管理するフロントエンドと、コンピュータープレイヤーの思考ルーチン(AI)を提供するバックエンドをGoogle Container Engine (GKE) 上のコンテナでデプロイします。
・ゲームのステータスは、Cloud Datastore(NoSQL)に保存します。

デモのシナリオはこんな感じ。

・はじめは(ゲームのリリーススケジュールにAIの開発が間に合わなかったため!)ランダムな手を打つダミーのAIでゲームをリリースします。
・その後、ちゃんとしたAIの開発が終わって、バックエンドのAIコンテナをこっそりアップデートします。
・KubernetesのRolling Update機能を使うので、ゲームのプレイヤーは、ゲームの途中で、突然、コンピューターの打つ手が良くなることに気づきます。

ちなみに、バックエンドのAIのサンプルコードは「なんちゃって」です。TensorFlowが動いているわけではありません。また、以下の作業の前提として、GCPのプロジェクト作成、課金設定、Compute Engine, Datastore, Container Engine APIの有効化が必要です。

コンテナイメージの作成と動作確認

GCPでは、Cloud Shellと呼ばれる作業用インスタンスが無料で利用できます。Cloud Shell上では、Dockerが動いているので、Dockerfileからイメージを作成することもできます。

参考:Google Cloud Platform Japan Blog - Cloud Shell が GA リリース、料金は無料に

Cloud Consoleで下記のボタンをポチッとすると、Cloud Shellが起動します。Cloud Shellのインスタンスは、数時間アクセスしていないと自動的に削除されますが、ホームディレクリーの内容は保存されるようになっています。

次の手順でソースをダウンロードして、コンテナイメージを3つ作成します。

$ git clone https://github.com/GoogleCloudPlatform/gke-gobang-app-example.git
$ cd gke-gobang-app-example/
$ docker build -t frontend:v1.0 frontend/
$ docker build -t backend:v1.0 backend-dummy/
$ docker build -t backend:v1.1 backend-smart/

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
backend             v1.1                c1c5f9555252        3 seconds ago       286.2 MB
backend             v1.0                6c30c63cd6dc        34 seconds ago      286.1 MB
frontend            v1.0                0982f697e8dc        38 seconds ago      286.2 MB
debian              8.4                 7a4c9a4d5e7a        11 weeks ago        125.1 MB

・frontend:v1.0 ⇒ フロントエンド
・backend:v1.0 ⇒ ダミーAIのバックエンド
・backend:v1.1 ⇒ ちゃんとしたAIのバックエンド

※ Cloud Shell上でこのイメージをビルドすると、結構時間がかかります。気長にお待ち下さい。

作成したイメージをまずは、Cloud Shellのローカルインスタンス上のDockerで起動して、動作確認を行います。はじめのgcloudコマンドは、バックエンドのCloud Datastoreを準備するためのコマンドです。次のコマンドの "hogehoge" には、GCPのプロジェクトIDを指定してください。

$ gcloud app create --region=us-central
$ export PROJECT_ID="hogehoge"
$ docker run -d --name backend -e PROJECT_ID=$PROJECT_ID backend:v1.0
$ docker run -d --name frontend -p 8080:8080 -e PROJECT_ID=$PROJECT_ID --link backend:backend frontend:v1.0
$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                
    NAMES
9f5b27a40326        frontend:v1.0       "/opt/gobang/bin/fron"   4 seconds ago       Up 4 seconds        0.0.0.0:8080->8080/tcp   frontend
690787b5415b        backend:v1.0        "/opt/gobang/bin/back"   11 seconds ago      Up 10 seconds       8081/tcp                 backend

かっちょいいUIのクライアントAPPはまだないので、コンソール上で動作するCUIのアプリでゲームを遊んでみます。環境変数 API_URL でフロントエンドのAPIを指定して実行します。

$ API_URL=http://localhost:8080/api/v1 client/client.py
Welcome to the five-stone game.
Game ID (0:new game)? 0
Your game ID is 5649391675244544

  0 1 2 3 4 5 6 7 8 9
0 - - - - - - - - - - 
1 - - - - - - - - - - 
2 - - - - - - - - - - 
3 - - - - - - - - - - 
4 - - - - - - - - - - 
5 - - - - - - - - - - 
6 - - - - - - - - - - 
7 - - - - - - - - - - 
0 - - - - - - - - - - 
8 - - - - - - - - - - 
9 - - - - - - - - - - 
(q:quit) x(0-9), y(0-9)? 4,5

  0 1 2 3 4 5 6 7 8 9
0 - - - - - - - - - - 
1 - - - - - - - - x - 
2 - - - - - - - - - - 
3 - - - - - - - - - - 
4 - - - - - - - - - - 
5 - - - - o - - - - - 
6 - - - - - - - - - - 
7 - - - - - - - - - - 
8 - - - - - - - - - - 
9 - - - - - - - - - - 
(q:quit) x(0-9), y(0-9)? 5,5

  0 1 2 3 4 5 6 7 8 9
0 - - - - - - - - - - 
1 - - - - - - - - x - 
2 - - - - - - - - - - 
3 - - - - - - - - - - 
4 - - - - - - - - - - 
5 - - - - o o - - - - 
6 - - - - - - - - - - 
7 - - - - - - - - - - 
8 - - - - - - - - x - 
9 - - - - - - - - - - 
(q:quit) x(0-9), y(0-9)? q
Your game ID is 5649391675244544
See you again.

遊び方は画面の様子から察してください。。。。ゲームの開始時にゲームIDが割り当てられるので、途中でゲームを中断した場合でも、ゲームIDを指定してゲームを再開することができます。ここでは、ダミーAIのバックエンドを使っているので、コンピューターの手はランダムです。ローカルのDocker環境を使用する場合、新しいバックエンドに入れ替える際は、一度、コンテナを停止する必要があります。サービスを提供したままこっそりアップデートするということはできません。

動作確認ができたら、ローカルのコンテナは停止・破棄しておきます。

$ docker stop frontend backend
$ docker rm frontend backend

コンテナイメージのアップロード

GKEのクラスターからコンテナイメージを利用できるように、GCP上のプライベートレジストリーにイメージをアップロードしておきます。次のように、「gcr.io/<PROJECT ID>/名前:タグ」というイメージ名を付けて、gcloudコマンドからpushします。(gcloudコマンドを使用することにより、Cloud Shellを起動したアカウントの権限でプライベートレジストリーへのアクセスが行われます。)

$ docker tag frontend:v1.0 gcr.io/$PROJECT_ID/frontend:v1.0
$ docker tag backend:v1.0 gcr.io/$PROJECT_ID/backend:v1.0
$ docker tag backend:v1.1 gcr.io/$PROJECT_ID/backend:v1.1

$ gcloud docker -- push gcr.io/$PROJECT_ID/frontend:v1.0
$ gcloud docker -- push gcr.io/$PROJECT_ID/backend:v1.0
$ gcloud docker -- push gcr.io/$PROJECT_ID/backend:v1.1

$ gcloud docker -- search gcr.io/$PROJECT_ID
NAME                     DESCRIPTION   STARS     OFFICIAL   AUTOMATED              
<PROJECT ID>/backend                   0                    
<PROJECT ID>/frontend                  0       

アップロードしたイメージの実体は、自分のプロジェクトのCloud Storage(バケット名「artifacts.<PROJECT ID>.appspot.com」)に保存されています。

$ gsutil ls | grep artifacts
gs://artifacts.<PROJECT ID>.appspot.com/

プライベートレジストリーの内容は、Cloud Consoleからも確認できます。


コンテナクラスターの作成

Cloud Consoleからコンテナクラスターを作成します。

ゾーンは、プロジェクト作成時に指定したApp Engineのリージョンと同じ物を指定します。Cloud Datastoreは、App Engineと同じリージョンにデータを保存するので、Cloud Datastoreへのアクセスが早くなります。また、「プロジェクトへのアクセス」で「Cloud Datastore」を「有効」にしてください。これを忘れるとコンテナからCloud Datastoreへのアクセスができません。


クラスターが作成できたら、Cloud Shellから次のコマンドを実行して、クラスターを操作するための環境設定を行います。「gobang-cluster」と「us-central1-a」は、クラスター名とクラスターを作成したゾーンを指定します。

$ gcloud container clusters get-credentials gobang-cluster --zone=us-central1-a

これを実行すると設定ファイル「~/.kube/config」が用意されて、Cloud Shellからkubectlコマンドで操作できるようになります。次は、クラスター内のノードを確認する例です。

$ kubectl get nodes
NAME                                            STATUS    AGE
gke-gobang-cluster-default-pool-d43cc941-fkwy   Ready     6m
gke-gobang-cluster-default-pool-d43cc941-kbnf   Ready     6m
gke-gobang-cluster-default-pool-d43cc941-w53o   Ready     6m

バックエンドのデプロイ

デプロイ設定「config/backend-deployment.yaml」を開いて、コンテナイメージ名に含まれる<PROJECT ID>の部分を実際に使用するプロジェクトIDを修正します。

    spec:
      containers:
      - image: gcr.io/<PROJECT ID>/backend:v1.0 <-- ココ
        name: backend-node
        ports:
        - containerPort: 8081

デプロイ設定を指定して、コンテナをデプロイします。下記のように、3個のPodがRunningになれば成功です。

$ kubectl create -f config/backend-deployment.yaml 
deployment "backend-node" created

$ kubectl get pods
NAME                            READY     STATUS    RESTARTS   AGE
backend-node-3459171109-09yzl   1/1       Running   0          5s
backend-node-3459171109-2gs4u   1/1       Running   0          5s
backend-node-3459171109-tmkjl   1/1       Running   0          5s

フロントエンドのコンテナからバックエンドに接続できるように、サービスを定義します。

$ kubectl create -f config/backend-service.yaml 
service "backend-service" created

$ kubectl get services
NAME              CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
backend-service   10.19.248.188   <none>        8081/TCP   6s
kubernetes        10.19.240.1     <none>        443/TCP    19m

上記の「CLUSTER-IP」がコンテナにアクセスするための代表IPになります。この例では、「10.19.248.188」が割り当てられています。他のコンテナからこのIPにアクセスすると、自動的に複数のバックエンドコンテナへの負荷分散が行われます。また、コンテナ内部では専用のDNSが用意されており、「backend-service.default.svc.cluster.local」というホスト名にアクセスすると、先ほどの代表IPへのアクセスが行われます。

フロントエンドのデプロイ

デプロイ設定「config/frontend-deployment.yaml」を開いて、コンテナイメージ名に含まれる<PROJECT ID>の部分を実際に使用するプロジェクトIDを修正します。

    spec:
      containers:
      - image: gcr.io/<PROJECT ID>/frontend:v1.0 <-- ココ
        name: frontend-node
        ports:
        - containerPort: 8080

デプロイ設定を指定して、コンテナをデプロイします。下記のように、3個のPodがRunningになれば成功です。

$ kubectl create -f config/frontend-deployment.yaml 
deployment "frontend-node" created

$ kubectl get pods
NAME                             READY     STATUS    RESTARTS   AGE
backend-node-3459171109-09yzl    1/1       Running   0          6m
backend-node-3459171109-2gs4u    1/1       Running   0          6m
backend-node-3459171109-tmkjl    1/1       Running   0          6m
frontend-node-3555902700-el65p   1/1       Running   0          7s
frontend-node-3555902700-irc2x   1/1       Running   0          7s
frontend-node-3555902700-jvfkm   1/1       Running   0          7s

外部からフロントエンドにアクセスできるように、サービスを定義します。

$ kubectl create -f config/frontend-service.yaml 
service "frontend-service" created

こちらのサービス定義ファイルには、「type: LoadBalancer」という指定があります。これによって、GCPのGlobal Load Balancerが自動的に構成されて、ロードバランサー経由でフロントエンドにアクセスできるようになります。しばらく待つと、下記のように「EXTERNAL-IP」がセットされて、これが外部からアクセスする際のIPアドレスになります。(外部アクセス用のIPアドレスは自動的に割り当てるのではなく、事前に確保しておいたIPを明示的に指定することもできます。)

$ kubectl get services
NAME               CLUSTER-IP      EXTERNAL-IP      PORT(S)    AGE
backend-service    10.19.248.188   <none>           8081/TCP   10m
frontend-service   10.19.242.12    104.154.52.102   80/TCP     2m
kubernetes         10.19.240.1     <none>           443/TCP    30m

クライアント側では、このIPアドレスを指定することで、ゲームを遊べるようになります。次は、ゲームIDを指定して、先ほどの続きを遊ぶ例です。

$ API_URL=http://104.154.52.102/api/v1 client/client.py 
Welcome to the five-stone game.
Game ID (0:new game)? 5649391675244544
Your game ID is 5649391675244544
  0 1 2 3 4 5 6 7 8 9
0 - - - - - - - - - - 
1 - - - - - - - - x - 
2 - - - - - - - - - - 
3 - - - - - - - - - - 
4 - - - - - - - - - - 
5 - - - - o o - - - - 
6 - - - - - - - - - - 
7 - - - - - - - - - - 
8 - - - - - - - - x - 
9 - - - - - - - - - - 
(q:quit) x(0-9), y(0-9)? 4,4

  0 1 2 3 4 5 6 7 8 9
0 - - - - - - - - - - 
1 - - - - - - - - x - 
2 - - - - - - - - - - 
3 - - - - - - - - - - 
4 - - - - o - - - - - 
5 - - - - o o - - - - 
6 - - x - - - - - - - 
7 - - - - - - - - - - 
8 - - - - - - - - x - 
9 - - - - - - - - - - 
(q:quit) x(0-9), y(0-9)? 

ただし、バックエンドのAIは相変わらずダミーです。。。。

バックエンドのアップデート

ここで、先ほどのクライアントを停止せずに、こっそりとバックエンドをアップデートします。Cloud Shellの画面の上にある「+」ボタンでシェル画面を新しく開いたら、次のコマンドを実行します。

$ kubectl edit deployment/backend-node

バックエンドのデプロイメント設定がエディターで開くので、イメージのタグ名を「v1.1」に修正して保存します。

    spec:
      containers:
      - image: gcr.io/<PROJECT ID>/backend:v1.1 <--ココ
        imagePullPolicy: IfNotPresent
        name: backend-node

次のように、バックエンドのPodが再デプロイされていることがわかります。

$ kubectl get pods
NAME                             READY     STATUS              RESTARTS   AGE
backend-node-3540566822-0ukin    0/1       ContainerCreating   0          13s
backend-node-3540566822-2n0u3    1/1       Running             0          13s
backend-node-3540566822-hsxys    1/1       Running             0          11s
frontend-node-3555902700-el65p   1/1       Running             0          13m
frontend-node-3555902700-irc2x   1/1       Running             0          13m
frontend-node-3555902700-jvfkm   1/1       Running             0          13m

デプロイ設定のヒストリーを確認すると、新しいイメージのPodを起動した後に、古いイメージのPodを停止するという操作が順番に行われたことがわかります。

$ kubectl describe deployment/backend-node
Name:                   backend-node
Namespace:              default
CreationTimestamp:      Wed, 10 Aug 2016 14:54:53 +0900
Labels:                 name=backend-node
Selector:               name=backend-node
Replicas:               3 updated | 3 total | 3 available | 0 unavailable
StrategyType:           RollingUpdate
MinReadySeconds:        0
RollingUpdateStrategy:  1 max unavailable, 1 max surge
OldReplicaSets:         <none>
NewReplicaSet:          backend-node-3540566822 (3/3 replicas created)
Events:
  FirstSeen     LastSeen        Count   From                            SubobjectPath   Type            Reason   Message
  ---------     --------        -----   ----                            -------------   --------        ------   -------
  19m           19m             1       {deployment-controller }                        Normal          ScalingReplicaSet Scaled up replica set backend-node-3459171109 to 3
  36s           36s             1       {deployment-controller }                        Normal          ScalingReplicaSet Scaled up replica set backend-node-3540566822 to 1
  36s           36s             1       {deployment-controller }                        Normal          ScalingReplicaSet Scaled down replica set backend-node-3459171109 to 2
  36s           36s             1       {deployment-controller }                        Normal          ScalingReplicaSet Scaled up replica set backend-node-3540566822 to 2
  34s           34s             1       {deployment-controller }                        Normal          ScalingReplicaSet Scaled down replica set backend-node-3459171109 to 1
  34s           34s             1       {deployment-controller }                        Normal          ScalingReplicaSet Scaled up replica set backend-node-3540566822 to 3
  33s           33s             1       {deployment-controller }                        Normal          ScalingReplicaSet Scaled down replica set backend-node-3459171109 to 0

クライアントの画面に戻ってゲームを続けると、コンピューターがまともな手を打つように変わっています。やったー。

  0 1 2 3 4 5 6 7 8 9
0 - - - - - - - - - - 
1 - - - - - - - - x - 
2 - - - - - - - - - - 
3 - - - - - - - - - - 
4 - - - - o - - - - - 
5 - - - - o o - - - - 
6 - - x - - - - - - - 
7 - - - - - - - - - - 
8 - - - - - - - - x - 
9 - - - - - - - - - - 
(q:quit) x(0-9), y(0-9)? 4,6

  0 1 2 3 4 5 6 7 8 9
0 - - - - - - - - - - 
1 - - - - - - - - x - 
2 - - - - - - - - - - 
3 - - - - - - - - - - 
4 - - - - o - - - - - 
5 - - - - o o - - - - 
6 - - x - o - - - - - 
7 - - - - x - - - - - 
8 - - - - - - - - x - 
9 - - - - - - - - - - 
(q:quit) x(0-9), y(0-9)? 4,3

  0 1 2 3 4 5 6 7 8 9
0 - - - - - - - - - - 
1 - - - - - - - - x - 
2 - - - - x - - - - - 
3 - - - - o - - - - - 
4 - - - - o - - - - - 
5 - - - - o o - - - - 
6 - - x - o - - - - - 
7 - - - - x - - - - - 
8 - - - - - - - - x - 
9 - - - - - - - - - - 
(q:quit) x(0-9), y(0-9)? 

後片付け

サービスとデプロイを削除します。サービスを削除したタイミングで、Global Load Balancerの設定も破棄されます。

$ kubectl delete service frontend-service
$ kubectl delete service backend-service
$ kubectl delete deployment frontend-node
$ kubectl delete deployment backend-node

この後は、Cloud Consoleからコンテナクラスターの削除、Cloud Storageのプライベートレジストリー用バケットの削除、Cloud Datastoreに保存されたEntity(「GameBoards」というKind)の削除などを行います。


Disclaimer: All code snippets are released under Apache 2.0 License. This is not an official Google product.