本資料は、Weaveworksが公開しているマイクロサービスのデモアプリケーションSock Shopを利用(on OpenShift)して、マイクロサービスを体験するハンズオンです。
- OpenShift 4.8
https://github.com/microservices-demo/microservices-demo
Sock Shopのデザインは下記の通りで、Java, NodeJS, Goなどとマイクロサービスの特徴であるポリグロットを体現しています。詳細はこちら。
- openshiftクラスタ
- Google Chrome
- ocコマンドのインストール
- curlのインストール
- Githubアカウント
- 本レポジトリをフォークしてください。
- jqのインストール (recommended)
- postmanのインストール (optional)
本レポジトリのマニフェストファイルは、SCCの変更やcluster-adminを持っていなくてもOpenShift上で動くように修正しています。
$ export OCP_USER=userX
$ oc new-project $OCP_USER-sockshop
$ oc apply -f complete-demo.yaml -n $OCP_USER-sockshop
deployment.extensions/carts-db created
service/carts-db created
deployment.extensions/carts created
service/carts created
deployment.extensions/catalogue-db created
service/catalogue-db created
deployment.extensions/catalogue created
service/catalogue created
deployment.extensions/front-end created
service/front-end created
deployment.extensions/orders-db created
service/orders-db created
deployment.extensions/orders created
service/orders created
deployment.extensions/payment created
service/payment created
deployment.extensions/queue-master created
service/queue-master created
deployment.extensions/rabbitmq created
service/rabbitmq created
deployment.extensions/shipping created
service/shipping created
deployment.extensions/user-db created
service/user-db created
deployment.extensions/user created
service/user created
$ oc get pod
NAME READY STATUS RESTARTS AGE
carts-db-86b89bcb4c-m68xh 1/1 Running 0 57s
carts-ffc5d77cd-9vf84 1/1 Running 0 56s
catalogue-6d8895f8b9-tjxk8 1/1 Running 0 52s
catalogue-db-746b88dc5c-mfg8p 1/1 Running 0 54s
front-end-f975554b6-f9qrc 1/1 Running 0 51s
orders-65df94c9f5-786hf 1/1 Running 0 48s
orders-db-5ff5cdd64c-cv4cb 1/1 Running 0 49s
payment-f5dcc89c5-6k2jl 1/1 Running 0 46s
queue-master-787b68b7fd-sjsxb 1/1 Running 0 45s
rabbitmq-7db7568778-89t2h 1/1 Running 0 43s
shipping-5c6867f94f-tzwdg 1/1 Running 0 42s
user-6866b4bcf4-rbr7p 1/1 Running 0 38s
user-db-78c5b86bb8-njsz8 1/1 Running 0 40s
Routeオブジェクトが作成され、Sock Shopにアクセスできます。
$ oc get route
NAME HOST/PORT PATH SERVICES PORT TERMINATION WILDCARD
front-end front-end-sock-shop.apps.8aad.example.opentlc.com front-end web None
ブラウザを起動して、作成したrouteのURLに接続して以下のようなSock Shopの画面が返ってくるか確認しましょう。
まずは、Sock Shopのサービスをブラウザから触って概要を理解しよう。
- ユーザ登録をする
- デフォルトでユーザは登録されていない
- カタログから靴下を検索、閲覧する
- カートに入れてみる
- カートの中身を確認する
- ペイメントと住所を登録せずに、何かをオーダーする。エラーでオーダーできない。
- ペイメントの登録する
- 住所の登録する
- もういちどオーダーする
- オーダーの詳細をみる
Sock Shopで利用されているデータを書き出してみましょう。 後ほど実際にデータベースの構造を見ていきますが、現時点でどんなデータがどこにあるか意識しておくといいでしょう。
どのサービスがどのようなデータを保持しているかは、非常に重要です。 どのサービスがデータベースをもっていて、どのような情報をもっているか確かめてみましょう。
- catalogue(mysql)
- user(monogo)
- cart(mongo)
- order(mongo)
例として、データ構造の確認方法を記載します。
registry.gitlab.com/mosuke5/debug-container
は、mysqlとmongodbのクライアントをインストールしてあるCentOSベースのイメージです。
$ oc run debug --image registry.gitlab.com/mosuke5/debug-container -it /bin/bash
# mysql -uroot -pfake_password socksdb -h catalogue-db
mysql>
mysql> show tables;
+-------------------+
| Tables_in_socksdb |
+-------------------+
| sock |
| sock_tag |
| tag |
+-------------------+
3 rows in set (0.01 sec)
mysql>
mysql> desc sock;
???
# mongo -u sock-user -p password user-db/users
> show collections;
???
> db.customers.find()
???
# mongo -u sock-user -p password carts-db/data
???
# mongo -u sock-user -p password orders-db/data
???
mysql> desc sock;
+-------------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------------+--------------+------+-----+---------+-------+
| sock_id | varchar(40) | NO | PRI | NULL | |
| name | varchar(20) | YES | | NULL | |
| description | varchar(200) | YES | | NULL | |
| price | float | YES | | NULL | |
| count | int(11) | YES | | NULL | |
| image_url_1 | varchar(40) | YES | | NULL | |
| image_url_2 | varchar(40) | YES | | NULL | |
+-------------+--------------+------+-----+---------+-------+
mysql> desc sock_tag;
+---------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------+--------------+------+-----+---------+-------+
| sock_id | varchar(40) | YES | MUL | NULL | |
| tag_id | mediumint(9) | NO | MUL | NULL | |
+---------+--------------+------+-----+---------+-------+
mysql> desc tag;
+--------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------+--------------+------+-----+---------+----------------+
| tag_id | mediumint(9) | NO | PRI | NULL | auto_increment |
| name | varchar(20) | YES | | NULL | |
+--------+--------------+------+-----+---------+----------------+
mysql> select * from tag;
+--------+--------+
| tag_id | name |
+--------+--------+
| 1 | brown |
| 2 | geek |
| 3 | formal |
| 4 | blue |
| 5 | skin |
| 6 | red |
| 7 | action |
| 8 | sport |
| 9 | black |
| 10 | magic |
| 11 | green |
+--------+--------+
> show collections;
addresses
cards
customers
> db.customers.find()
{
"_id" : ObjectId("57a98d98e4b00679b4a830af"),
"firstName" : "Eve",
"lastName" : "Berger",
"username" : "Eve_Berger",
"password" : "fec51acb3365747fc61247da5e249674cf8463c2",
"salt" : "c748112bc027878aa62812ba1ae00e40ad46d497",
"addresses" : [ ObjectId("57a98d98e4b00679b4a830ad") ],
"cards" : [ ObjectId("57a98d98e4b00679b4a830ae") ]
}
> db.cards.find()
{
"_id" : ObjectId("57a98d98e4b00679b4a830ae"),
"longNum" : "5953580604169678",
"expires" : "08/19",
"ccv" : "678"
}
> db.addresses.find()
{
"_id" : ObjectId("57a98d98e4b00679b4a830ad"),
"street" : "ebisu street",
"number" : "1-1-1",
"country" : "japan",
"city" : "tokyo",
"postcode" : "111-1111",
"links" : { }
}
> show collections
cart
item
> db.cart.find()
{
"_id" : ObjectId("5e20278e03277a00079dd215"),
"_class" : "works.weave.socks.cart.entities.Cart",
"customerId" : "5e184694b6c6850001a762da",
"items" : [
DBRef("item", ObjectId("5e2115f003277a00079dd217"))
]
}
> db.item.find()
{
"_id" : ObjectId("5e2115f003277a00079dd217"),
"_class" : "works.weave.socks.cart.entities.Item",
"itemId" : "510a0d7e-8e83-4193-b483-e27e09ddc34d",
"quantity" : 1,
"unitPrice" : 15
}
...
> show collections;
customerOrder
> db.customerOrder.find()
{
"_id" : ObjectId("5e197a445196950008004bf1"),
"_class" : "works.weave.socks.orders.entities.CustomerOrder",
"customerId" : "5e184694b6c6850001a762da",
"customer" : {
"_id" : null,
"firstName" : "shinya",
"lastName" : "mori",
"username" : "mosuke5",
"addresses" : [ ],
"cards" : [ ]
},
"address" : {
"_id" : null,
"number" : "1-1-1",
"street" : "ebisu street",
"city" : "tokyo",
"postcode" : "111-1111",
"country" : "japan"
},
"card" : {
"_id" : null,
"longNum" : "111111111",
"expires" : "03.22",
"ccv" : "111"
},
"items" : [
{
"_id" : ObjectId("5e184c4b03277a00079dd204"),
"itemId" : "3395a43e-2d88-40de-b95f-e00e1502085b",
"quantity" : 1,
"unitPrice" : 18
},
{
"_id" : ObjectId("5e197a2703277a00079dd206"),
"itemId" : "510a0d7e-8e83-4193-b483-e27e09ddc34d",
"quantity" : 1,
"unitPrice" : 15
},
{
"_id" : ObjectId("5e197a2f03277a00079dd207"),
"itemId" : "808a2de1-1aaa-4c25-a9b9-6612e8f29a38",
"quantity" : 1,
"unitPrice" : 17.31999969482422
},
{
"_id" : ObjectId("5e197a3203277a00079dd208"),
"itemId" : "819e1fbf-8b7e-4f6d-811f-693534916a8b",
"quantity" : 1,
"unitPrice" : 14
},
{
"_id" : ObjectId("5e197a3a03277a00079dd209"),
"itemId" : "d3588630-ad8e-49df-bbd7-3167f7efb246",
"quantity" : 1,
"unitPrice" : 10.989999771118164
},
{
"_id" : ObjectId("5e197a3e03277a00079dd20a"),
"itemId" : "a0a4f044-b040-410d-8ead-4de0446aec7e",
"quantity" : 1,
"unitPrice" : 7.989999771118164
}
],
"shipment" : {
"_id" : "70a338f4-f40c-4512-86c7-5ad6ed8d57eb",
"name" : "5e184694b6c6850001a762da"
},
"date" : ISODate("2020-01-11T07:33:24.861Z"),
"total" : 88.29000091552734
}
APIの仕様はどのように定義すればいいでしょうか。また、どのようにその定義を確認したら良いでしょうか。
OpenAPIやSwaggerぜひ学んでみましょう。
Sock ShopのAPIドキュメント
https://microservices-demo.github.io/api/index
APIの開発や検証では、欠かせないツールがあります。 CurlやPostmanの使い方はよく覚えておくことをおすすめします。 詳細な使い方は、ここでは割愛しますが、本ページに出てくるいくつかの基本的な項目について記載します。
curlではcurl https://google.com
と指定すると
デフォルトではGETメソッドを利用します。
システムのAPIではGETのほかにPOSTやDELETEなどのメソッドを使うことも多くあります。
-X
オプションで指定することができます。
curl -X POST https://xxxxx/user
POSTなどのメソッドを利用する場合は、リクエストともにデータを送信することがよくあります。
例えばユーザ情報の登録APIにおいては、登録するデータを送信する必要があります。
データの形式はAPI仕様によるので、必ず確認してください。
下記では、jsonでname
という項目が必要な場合の例です。
$ curl -XPOST \
-d '{"name":"mosuke5"}' \
-H 'Content-Type:application/json; charset=UTF-8' \
https://xxxxx/user
ユーザエージェントやコンテンツタイプなどの指定でHTTPヘッダーを追加することがあります。
$ curl -H 'User-Agent: hoge' https://xxxxx/
APIによっては、認証が必要なことがあります。 例えば、ユーザAの個人情報がユーザBから取得できてしまってはいけません。 認証の方法もAPI仕様によるので、必ず確認しましょう。 本ワークショップでは、クッキーを利用するのでその方法を紹介します。 詳しくは、「ログイン」の項目で実践してみましょう。
curl -XGET -c cookie.txt https://xxxxx/login
curl
でAPIを実行した際のレスポンスはJSON形式ですが、なにもしないと1行にまとめられていて見づらいです。
jq
かpython
などを使って見やすく表示しましょう。
$ curl -X GET http://xxxxx/ | jq .
{
"id": "3395a43e-2d88-40de-b95f-e00e1502085b",
"name": "Colourful",
"description": "proident occaecat irure et excepteur labore minim nisi amet irure",
"imageUrl": [
"/catalogue/images/colourful_socks.jpg",
"/catalogue/images/colourful_socks.jpg"
],
$ curl -X GET http://xxxxx/ | python -mjson.tool
{
"id": "3395a43e-2d88-40de-b95f-e00e1502085b",
"name": "Colourful",
"description": "proident occaecat irure et excepteur labore minim nisi amet irure",
"imageUrl": [
"/catalogue/images/colourful_socks.jpg",
"/catalogue/images/colourful_socks.jpg"
],
フロントエンドアドレスを環境変数に指定しておきます。
$ export FRONTEND_ADDRESS=<your-frontend-address>
カタログ情報はログインなしでも実行できる簡単なAPIを実行してみよう。
APIドキュメントを見ながらURIとパラメータを確認するといいです。
商品ID 3395a43e-2d88-40de-b95f-e00e1502085b
についても検索してみよう。
$ curl -X GET $FRONTEND_ADDRESS/catalogue/3395a43e-2d88-40de-b95f-e00e1502085b
{
"id": "3395a43e-2d88-40de-b95f-e00e1502085b",
"name": "Colourful",
"description": "proident occaecat irure et excepteur labore minim nisi amet irure",
"imageUrl": [
"/catalogue/images/colourful_socks.jpg",
"/catalogue/images/colourful_socks.jpg"
],
"price": 18,
"count": 438,
"tag": [
"brown",
"blue"
]
}
商品のオーダーなどは当然ながらログインした状態でないと実行できません。
試しに、ログインセッションを持たないまま、オーダーAPIを実行してみるとログインしてくれとエラーが返ってきます。
$ curl -XGET $FRONTEND_ADDRESS/orders
{"message":"User not logged in.","error":{}}
ログインには"username:password"をbase64でエンコードしたものが必要です(該当コード)。
コマンドラインで生成するか、こちらのようなBase64エンコードをしてくれるWebサービスで生成しましょう。
$ echo -n "user:password" | base64
dXNlcjpwYXNzd29yZA==
$ curl -XGET -c cookie.txt -H "Authorization: Basic dXNlcjpwYXNzd29yZA==" -v $FRONTEND_ADDRESS/login
$ cat cookie.txt
# Netscape HTTP Cookie File
# https://curl.haxx.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.
xxxxxxx
ログインができているか確認します。オーダー情報が返ってきていれば成功です。
$ curl -XGET -b cookie.txt $FRONTEND_ADDRESS/orders
[
{
"customerId": "57a98d98e4b00679b4a830b2",
"customer": {
"firstName": "User",
"lastName": "Name",
"username": "user",
"addresses": [],
"cards": []
},
"address": {
"number": "246",
"street": "Whitelees Road",
"city": "Glasgow",
"postcode": "G67 3DL",
"country": "United Kingdom"
},
"card": {
"longNum": "5544154011345918",
"expires": "08/19",
"ccv": "958"
},
"items": [
{
"itemId": "808a2de1-1aaa-4c25-a9b9-6612e8f29a38",
"quantity": 1,
"unitPrice": 17.32
}
],
"shipment": {
"name": "57a98d98e4b00679b4a830b2"
},
"date": "2019-12-23T06:35:49.925+0000",
"total": 22.31,
"_links": {
"self": {
"href": "http://orders/orders/5e006045dc2f2e0006f35ee4"
},
"order": {
"href": "http://orders/orders/5e006045dc2f2e0006f35ee4"
}
}
}
]
API経由でカートに商品を追加してみましょう。 その後にカートの中身を確認してみましょう。
$ curl -XPOST -b cookie.txt \
-d '{"id":"3395a43e-2d88-40de-b95f-e00e1502085b"}' \
-H 'Content-Type:application/json; charset=UTF-8' \
$FRONTEND_ADDRESS/cart
$ curl -XGET -b cookie.txt $FRONTEND_ADDRESS/cart
[
{
"id": "5e006ff5ea192a0006ead37d",
"itemId": "3395a43e-2d88-40de-b95f-e00e1502085b",
"quantity": 2,
"unitPrice": 18
}
]
同じ要領でAPI経由でオーダーしてみよう。 その後に、オーダーしたものがオーダーリストに入っているかAPI経由で同じく確認してみましょう。
$ curl -XPOST -b cookie.txt $FRONTEND_ADDRESS/orders
{"id":"5e008861dc2f2e0006f35ee6","customerId":"57a98d98e4b00679b4a830b2","customer":{"id":null,"firstName":"User","lastName":"Name","username":"user","addresses":[],"cards":[]},"address":{"id":null,"number":"246","street":"Whitelees Road","city":"Glasgow","postcode":"G67 3DL","country":"United Kingdom"},"card":{"id":null,"longNum":"5544154011345918","expires":"08/19","ccv":"958"},"items":[{"id":"5e006ff5ea192a0006ead37d","itemId":"3395a43e-2d88-40de-b95f-e00e1502085b","quantity":3,"unitPrice":18}],"shipment":{"id":"33c17bcf-2b1e-4a1b-83a2-c02a79b032cc","name":"57a98d98e4b00679b4a830b2"},"date":"2019-12-23T09:26:57.477+0000","total":58.989998
$ curl -XGET -b cookie.txt $FRONTEND_ADDRESS/orders
[
{
"customerId": "57a98d98e4b00679b4a830b2",
"customer": {
"firstName": "User",
"lastName": "Name",
"username": "user",
"addresses": [],
"cards": []
},
"address": {
"number": "246",
"street": "Whitelees Road",
"city": "Glasgow",
"postcode": "G67 3DL",
"country": "United Kingdom"
},
"card": {
"longNum": "5544154011345918",
"expires": "08/19",
"ccv": "958"
},
"items": [
{
"itemId": "808a2de1-1aaa-4c25-a9b9-6612e8f29a38",
"quantity": 1,
"unitPrice": 17.32
}
],
"shipment": {
"name": "57a98d98e4b00679b4a830b2"
},
"date": "2019-12-23T06:35:49.925+0000",
"total": 22.31,
"_links": {
"self": {
"href": "http://orders/orders/5e006045dc2f2e0006f35ee4"
},
"order": {
"href": "http://orders/orders/5e006045dc2f2e0006f35ee4"
}
}
}
]
フロントエンドサービスの役割を考えてみよう。
ひとつひとつのAPIがどんなものかわかってきたところで、フロントエンドサービスのコードをのぞいてみよう。
- メリット
- アプリケーションの内部仕様を隠蔽できる
- クライアントからみて、各サービスの接続先を気にしなくて良い
- クライアントサイドを簡素化
- クライアントからのリクエスト数の削減
- 認証認可などのオフロード
- デメリット
- レスポンスタイムの増加
- コンポーネントが増えることによる複雑化
- ボトルネックの可能性
- Gatewayのモノリシック化
API ゲートウェイ パターンと、クライアントからマイクロサービスへの直接通信との比較
例えば、オーダー詳細をブラウザからみてみよう。オーダーの情報はもちろん商品の情報も表示されている。しかし、オーダーAPIではitemのidしか返していません。 どのようにしてitemの情報を取得しているか、 ブラウザのデベロッパーツール(検証モード)から確認してみましょう。
また、モノリスなサービスの場合と比べてどうか、対策する方法があるか考えてみましょう。
ブラウザのデベロッパーツールを利用するとわかるが、オーダー情報を取得後に、オーダーしたitemを1つずつ取得しに行っていることがわかる。
コードレベルでもこちらの実装から伺える。
マイクロサービスアーキテクチャでは、サービス間の通信をどのように行うかは重要な選択の1つです。
Sock Shopでも、アーキテクチャデザインの図を見ると、shippingサービスはRabbitMQに対してデータを送り、queue-masterが処理していることがわかります。
shipping
サービスとqueue-master
サービスのPodのログを見ながら、画面上からオーダー処理を行ってみましょう。
ログからキューへの書き込みおよび、キューからの受信を確認してみましょう。
また、非同期型の通信を利用するメリットについても考えてみましょう。
マイクロサービスの回復性について考えてみます。
cartsサービスを落としてみて、どんな影響があるか確認してみよう。
$ oc scale --replicas=0 deployment/carts
- オーダーはできるか?
- 他のコンポーネントへの影響は?
- ユーザ体験はどう変わったか?
cartsサービスを落としたあと、UI上からカートボタンが消えたのに気づきましたか? これは、マイクロサービスでよくあるデグレードの実装が行われているからです。 できる人は、実際にデグレードの実装が行われているコードを探してみましょう。 該当のコード
ブラウザのデバッグツールを開いて、cartsサービスへの通信状況を見てみましょう。
アクセスの度にcartsサービスのタイムアウトを待っているのがわかるのではないでしょうか。
サーキットブレーカを実装した場合にはどのような動きになるか考えてみよう。
cartsサービスを落としたあとに、タイムアウトで30秒ほど待っている様子。
落としたcartsサービスを元に戻しましょう。
$ oc scale --replicas=1 deployment/carts
マイクロサービスのスケーリングについて考えてみます。
マイクロサービスでは特定のサービスのみをスケールすることが容易であることが特徴の1つです。
ためしにフロントエンドサービスをスケールさせてみてみましょう。
$ oc scale --replicas=3 deployment/front-end
フロントエンドのコンテナイメージを変更してデプロイしてみよう。
サンプルで、背景色を変更したフロントエンドのイメージを用意しました。利用したい人は下記のように入れ替えてデプロイしてみましょう。
下のサンプルを利用すると、背景が黄色から緑色に変更されます。
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: front-end
namespace: sock-shop
spec:
replicas: 1
template:
metadata:
labels:
name: front-end
spec:
containers:
- name: front-end
#image: mosuke5/front-end:master-49c0d1f
image: mosuke5/front-end:master-7ad6254
$ oc apply -f complete-demo.yaml
続いて、トレーシングを体験するためにjaegerなどのコンポーネントをデプロイします。
$ oc apply -f jaeger.yaml
$ oc expose service/jaeger-query
$ oc get route
NAME HOST/PORT PATH SERVICES PORT TERMINATION WILDCARD
jaeger-query jaeger-query-sock-shop.xxxxx jaeger-query query-http None
jaeger-query
のURLに接続し下記のような画面にアクセスできれば成功です。
トレーシングコンポーネント起動後に、もう一度Sock Shopで購入などの様々なアクションを実行しましょう。検索からorders
サービスで検索をし、orders: http:/orders
を探してみましょう。
paymentサービスに障害が起きたと仮定し、paymentサービスを落としたあとに、もう一度購入操作をしてみましょう。 この複雑な処理のなかでもどこでエラーが起きたか一目瞭然に確認することができます。
Istioを使ったサービスメッシュによるマイクロサービスの課題解決を体験する場合は続きで以下にトライしてみましょう。
Service Mesh for sockshop