何の話かというと
Dockerコンテナでアプリを作ると便利ですが、何でもかんでもコンテナに突っ込むと(たとえば、RDBとか)面倒な事も多くなります。
・スケーラビリティが必要
・機能単位のリファクタリング/アップデート(マイクロサービス化)が必要
という部分にフォーカスしてコンテナ化して、その他のパーツは、コンテナ以外の環境と組み合わせた方が幸せになれるかも知れません。
というわけで、GCPを利用して、こんな感じのアプリケーション環境を構築するデモの手順を紹介します。
・五目並べゲームの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.