API開発チュートリアル
このチュートリアルでは、APIの開発手順について説明します。
はじめに
このチュートリアルでは、Qmonus SDKを使って実際にアプリケーションを作成してみます。 Qmonus SDKのユースケースの一つである「クラウドのリソース管理」を題材に取り上げ、実際にGoogle CloudのComputeEngineのインスタンスを管理するAPI(Sample CRUD API)を作成します。
前提
Sample CRUD APIでは、Google CloudのCompute Engine APIを用います。 そのため以下ができることを前提としています。
- Google CloudでComputeEngineの権限を持つサービスアカウントを作成している。
- Google CloudのCompute Engine APIを用いてCompute EngineインスタンスをCRUDすることができる。
開発物
Google CloudのCompute Engine APIを用いて、Compute Engineインスタンスを管理するクラウドリソース管理APIを作成します。
Sample CRUD API
CREATE : インスタンスを作成する
READ : インスタンスの情報を取得する
UPDATE : インスタンスのマシンタイプを更新する
DELETE : インスタンスを削除する
Sample CRUD APIのAPI仕様
今から作るAPIの仕様は以下になります。
CRUD | memo | endpoint | request method | request body | response |
---|---|---|---|---|---|
CREATE | Google CloudにComputeEngineインスタンスを新規作成する | /sample_crud_api/instances | POST | {“instance_name”: “xxxxxxx”} | 200 OK |
READ | 指定したインスタンス情報を取得する | /sample_crud_api/instances/{instance_name} | GET | - | 200 OK, {“instances”: [*]} |
UPDATE | 該当のComputeEngineインスタンスのマシンタイプを更新する | /sample_crud_api/instances/{instance_name} | PUT | {“machineType”: “xxxxxxx”} | 200 OK |
DELETE | 該当のComputeEngineインスタンスを削除する | /sample_crud_api/instances/{instance_name} | DELETE | - | 200 OK |
Sample CRUD APIの実装
ここからSample CRUD APIの実装に入ります。 まず今から作るSample CRUD APIの全体を把握するため、アーキテクチャを確認します
Sample CRUD APIのアーキテクチャ
Sample CRUD APIのアーキテクチャは以下になります。
ATOM
まず、Google CloudのComputeEngineインスタンスをQmonus SDK上で管理するためにATOMを用意します。
ATOMには、ComputeEngineのインスタンスが持つ情報(projectId, zone, machineType, sourceImage, network)とインスタンスのステータスを保存するようにし、Qmonus SDK上でリソースの管理を行います。
Config
続いて、Configを用意します。
Google CloudのCompute Engine APIを実行するためにはGoogle Cloud側でのサービスアカウントが必要です。
こちらのサービスアカウントのキー情報をConfigに保管することで、Qmonus SDK上でCompute Engine APIを実行できるようにします。
Routing
次にルーティングの設定を行います。(図のAPI GW箇所)
ルーティングを行うことで、各シナリオでCompute Engine APIを実行した際に、一度APIGWを経由してGoogle Cloudに飛んでいくようにします。
これにより各Compute Engine APIの実行状況を把握できるようになります。
Scenario
次に各シナリオを作成します。
Compute Engine APIを実行する際に、まずアクセストークンを取得する必要があるため、アクセストークンを取得するシナリオを作成します。
各CRUD APIはCompute Engine APIを叩くために、まずこのアクセストークンを取得するシナリオを呼び出すことになります。
最後に、CRUDの各シナリオを作成します。
--
上記がSample CRUD APIの実装の流れになります。
ATOMの実装
ATOMとは
まずATOMとはQmonus SDKではClassと表現されています。 イメージとしては、Qmonus SDKのModelとPythonのClassが一緒になった概念になります。 Modelのようにカラムの定義が可能で、PythonのClassのようにクラスメソッド、インスタンスメソッドの定義も可能となっています。
ATOMの定義
さっそくATOMを定義してみます。 以下の手順で、SDKポータル上でATOMを定義します。
クラスの作成
[Workflow Scenario as a Service]>[Class]>[Create New Class]
からクラスを作成します。
入力は以下を参考にしてください。
項番 | 項目 | 入力値 | 説明 |
---|---|---|---|
1 | category | SampleCrudAPI | ATOMが属する分類を指定してください。この情報はクラスエディタでの階層表示のためのタグとしてのみ使用されます。 |
2 | workspace | 未指定 | ワークスペースを設定します。 |
3 | name | VmInstance | ATOMの名前を指定します(クラス名と同義)。ユニークである必要があります。 |
メタ情報の定義
Class Definitions
の設定値を以下の表を参考に設定してください。
項番 | 項目 | 入力値 | 説明 |
---|---|---|---|
1 | persistence | 選択(default) | 永続化モードを指定します。選択した場合、ATOM定義を保存するとクラス構造に対応するデータベーステーブルが自動的に生成され、データベースに対するCRUDの組込みメソッドが実装されます。 |
2 | abstract | 未選択(default) | 抽象クラスモードを指定します。 |
3 | api_generation | 選択(default) | API自動生成モードを指定します。API自動生成モードでは、ATOMをCRUDするためのRestful APIが自動的に生成されます。 |
Note
SDK Portalを触っていると、画面上に該当の項目が表示されていないことがあります。
その場合は↓の鉛筆マークを押下し、表示する項目を編集することで該当の項目を表示させてあげることができます。
identifier fieldの定義
[attributes]>[identifier]
に以下を入力します。
項番 | 項目 | 入力値 | 説明 |
---|---|---|---|
1 | title | instanceId | フィールド名を指定します |
2 | field_type | integer | フィールドの型を指定します。 |
3 | field_persistence | 選択(default) | フィールドの永続化モードを指定します。選択しない場合は本フィールドはデータベースカラムとして生成されません。 |
4 | field_immutable | 選択(default) | フィールドの値が不変かを指定します。選択した場合、本フィールドの値変更は許可されません。 |
5 | field_metadata* | { "POST": false } | フィールドに任意のメタ情報を定義します。メタ情報はdict型のデータを設定してください。 |
local fieldsの定義
以下の表を参考に設定してください。
項番 | 項目 | 入力値 | 説明 |
---|---|---|---|
1 | field_name | instanceName | identifier field 参照 |
2 | field_type | [String]を選択(default) | identifier field 参照 |
3 | field_persistence | 選択(default) | identifier field 参照 |
4 | field_nullable | 選択(default) | フィールドの値にNullを許容するかを指定します。 |
5 | field_immutable | 未選択(default) | identifier field 参照 |
6 | field_unique | 未選択(default) | フィールドが複合ユニークインデックスの対象かを指定します。 |
7 | field_format* | '[a-zA-Z0-9]' | フィールドのフォーマットをjsonschemaもしくは正規表現で指定します。 |
8 | field_metadata* | { "POST": true, "PUT": true } | identifier field 参照 |
同様に以下のlocal fields
の定義も行います。
- projectId
- zone
- machineType
- sourceImage
- network
- status
- gceId
各local fields
はfield_name
以外は変更する必要はありません。
また、local fields
はデフォルト値を設定することができます。
以下からデフォルト値を設定しておきます。
以下のlocal fields
へ、デフォルト値を設定してください。(設定の際にはサービスアカウントやGoogle Cloudのコンソールを確認してください)
- projectId
- zone
- machineType
カウンタの作成
[Transaction as a Service]>[Transaction Settings]
の画面右上にある[+]ボタンを押下すると、カウンタ作成画面が開きます。
以下の項目を入力してください。
項番 | 項目 | 入力値 | 説明 |
---|---|---|---|
1 | counter_type | Number | カウンタの種類を指定します。Number、UUID、Inventoryの3つのタイプがあります。 |
2 | workspace | 未入力 | ワークスペースを設定します。 |
3 | counter_name | instanceId | カウンタを一意に識別する名前を指定します。 |
4 | counter_format | $ | カウンタから返される値の形式を指定します。値は$で表されます。 |
5 | min_num | 1 | カウンタの最小値を指定します。 |
6 | max_num | 9999 | カウンタの最大値を指定します。 |
7 | padding | 選択しない | ゼロパディングモードを指定します。 |
入力後、画面右下部にある[Create Counter]
ボタンを押下します。
メソッド(instance_methods)の定義
以下のコードを先ほど定義したVmInstanceのinstance_methods>method_body
に入力する。
async def initialize(self, *args, **kwargs):
if not self.instanceId:
self.instanceId = int(await Counter.allocate("instanceId"))
ATOMの確認
上記でATOMの用意ができました。
Interactive shell
から、以下の入力をしてみて動作を確認してみましょう。
# instanceの作成
>>> test_instance = await atom.VmInstance(instanceName="test-instance01")↵
... ↵
↵
>>> print(test_instance.instanceId)↵
... ↵
↵
1
# 作成したinstanceの保存
>>> await test_instance.save()↵
... ↵
↵
# 作成された全てのinstanceの取得
>>> instances = await atom.VmInstance.retrieve()↵
... ↵
↵
>>> print(instances)↵
... ↵
↵
[VmInstance(instance='Vm1JbnN0YW5jZTo0MWRkMjM4ZTg4OGMxMWYwOWMyM2Q2MzdmZDljNjEzYw==', xid=None, xname=None, instanceId=1, instanceName='test-instance01', projectId='*********', zone='*********', machineType='*********', sourceImage=None, network=None, status=None, gceId=None)]
# instanceNameの変更
>>> await instances[0].save(instanceName="test-instance02")↵
... ↵
↵
# instanceIdでフィルタをかけてinstanceを取得
>>> instance01 = await atom.VmInstance.load(1)↵
... ↵
↵
>>> print(instance01)↵
... ↵
↵
# instancenameが変更されていることを確認
VmInstance(instance='Vm1JbnN0YW5jZTo0MWRkMjM4ZTg4OGMxMWYwOWMyM2Q2MzdmZDljNjEzYw==', xid=None, xname=None, instanceId=1, instanceName='test-instance02', projectId='*********', zone='*********', machineType='*********', sourceImage=None, network=None, status=None, gceId=None)
# instanceの削除
>>> await instance01.destroy()↵
... ↵
↵
>>> instances = await atom.VmInstance.retrieve()↵
... ↵
↵
>>> print(instances)↵
... ↵
↵
# instanceが削除されていることを確認
[]
Configの設定
Google CloudのAPIを呼び出すためにはサービスアカウントを用意し、サービスアカウントキーをもとにアクセストークンを取得する必要があります。 そのため、今回のSample CRUD APIでもGoogle Cloud APIを呼び出すためにConfigにサービスアカウントキー情報を置く必要があります。
まずGoogle Cloud上でサービスアカウントを作成し、サービスアカウントキーを取得してください。
次に以下のように[Workflow Scenario as a Service]>[Config]
からCreate New Config
をクリックします。
serviceはSampleCrudApi
と入力し、 workspaceは未入力で右下のCreate Config
をクリックします。
以下のようにConfigにSampleCrudApi
が作成されているのが確認できたら、右上のオレンジ色の編集マークをクリックします。
Service Configの項目にJSON形式のサービスアカウント情報を入力し、右上の保存ボタンを押してconfigを保存します。
これでConfigの設定は完了です。
Routingの設定
続いてルーティングを設定します。
[API Gateway as a Service]>[Routing]
を開きます。
Create New Routings
からひとつずつ設定します。
以下は/GoogleCloud/SampleCrudAPI/Create/{projectId}/{zone}
への接続をhttps://compute.googleapis.com/compute/v1/projects/{projectId}/zones/{zone}/instances
に振り替えたい場合のルーティング設定例になります。
Note
proxy: 外部サービスやAPIへのリクエストを中継するためのプロキシです。 ここではシナリオからGoogle Cloud APIへリクエストする際に、API GWを中継させるようにするため、このプロキシをかませるようにします。
authorities: リクエストを送信する際の認証情報や権限を定義する項目です。 ここではGoogle Cloud APIのURIにおけるドメインを入力しています。
target: リクエストを転送する先のURLやエンドポイントを設定します。リクエストの送信先であるGoogle Cloud APIのURLを入力しています。 またGoogle Cloud APIはHTTPS通信のためschemeはhttpsとしています。
Caution
Routingのpath
をコピペによって入力すると、最後尾に半角や全角の空白文字が付随してしまうことがあります。
空白文字が存在すると正しくルーティングできないので、以下の設定でも気をつけましょう。
これに倣って以下の表からルーティング設定を行います。
項番 | workspace | domain | scheme(proxy) | path(proxy) | authority | scheme(target) | path(target) |
---|---|---|---|---|---|---|---|
1 | 未設定 | default | http | /GoogleCloud/SampleCrudAPI/token | oauth2.googleapis.com | https | /token |
2 | 未設定 | default | http | /GoogleCloud/SampleCrudAPI/{projectId}/{zone} | compute.googleapis.com | https | /compute/v1/projects/{projectId}/zones/{zone}/instances |
3 | 未設定 | default | http | /GoogleCloud/SampleCrudAPI/{projectId}/{zone}/{instanceName} | compute.googleapis.com | https | /compute/v1/projects/{projectId}/zones/{zone}/instances/{instanceName} |
4 | 未設定 | default | http | /GoogleCloud/SampleCrudAPI/{projectId}/{zone}/{instanceName}/stop | compute.googleapis.com | https | /compute/v1/projects/{projectId}/zones/{zone}/instances/{instanceName}/stop |
5 | 未設定 | default | http | /GoogleCloud/SampleCrudAPI/{projectId}/{zone}/{instanceName}/start | compute.googleapis.com | https | /compute/v1/projects/{projectId}/zones/{zone}/instances/{instanceName}/start |
6 | 未設定 | default | http | /GoogleCloud/SampleCrudAPI/{projectId}/{zone}/{instanceName}/setMachineType | compute.googleapis.com | https | /compute/v1/projects/{projectId}/zones/{zone}/instances/{instanceName}/setMachineType |
以上でルーティングの設定は完了です。
アクセストークンの取得APIの実装
続いてアクセストークンを取得するシナリオを作成します。
以下から作成画面に遷移します。([Workflow Scenario as a Service]>[Scenario]>[Create New Scenario]
)
作成画面を開いたのち、以下のシナリオ定義を設定し、シナリオを作成します。
項番 | 項目 | 入力値 | 説明 |
---|---|---|---|
1 | category | SampleCrudApi | シナリオが属する分類を指定してください。この情報はシナリオエディタでの階層表示のためのタグとしてのみ使用されます |
2 | workspace | 未入力 | シナリオのワークスペースを設定します |
3 | name | GetAccessToken | シナリオの名前を指定します。ユニークである必要があります。 |
4 | method | GET | APIとして待機するHTTPメソッドを指定します。 |
5 | uri | /sample_crud_api/get_access_token | APIとして待機するHTTPパスを指定します。 |
6 | transaction | 未選択 | トランザクションサービスと連携します。今回は利用しません。 |
7 | routing_auto_generation_mode | 選択 | API Gatewayにルーティングを自動登録するか選択します。 |
Caution
Scenarioのuri
に関してもRoutingのpath
同様に、最後尾に空白文字があると正しくuriとして設定できません。
Scenarioの設定の際には注意しましょう。
ワークフローの設定
シナリオを作成したのち、以下のように[Scenario]>[script]
をクリックし、scriptブロックを作成します。
scriptの設定をします。
設定項目は以下になります。(Display Label
はデフォルトでは表示されないため、鉛筆マークから選択する)
Display Label: create jwt
Python code:
import jwt
import json
from datetime import datetime, timedelta
# -- JWTの生成
# Configに設定したサービスアカウントの秘密鍵とメールアドレスをセットする
private_key = __CONFIG__["private_key"]
client_email = __CONFIG__["client_email"]
current_time = datetime.now()
expiration_time = current_time + timedelta(hours=1)
payload = {
"iss": client_email,
"scope": "https://www.googleapis.com/auth/compute",
"aud": "https://oauth2.googleapis.com/token",
"exp": int(expiration_time.timestamp()),
"iat": int(current_time.timestamp()),
}
jwt_token = jwt.encode(payload, private_key, algorithm="RS256")
同様にもうひとつscriptブロックを作成し以下を設定します。
Display Label: get access token
Python code:
# -- アクセストークンの取得
r = await callout(
path=f"/GoogleCloud/SampleCrudAPI/token",
method="POST",
headers={
"Content-Type": "application/x-www-form-urlencoded"
},
body=f"grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion={jwt_token}"
)
if r.error:
raise Error(r.code, reason="POST failed")
access_token = json.loads(r.body)["access_token"]
context.session.finish({"access_token": access_token})
Variablesの設定
script間で変数を共有するために、workflow全体で利用できるglobal変数を定義する必要がある。 以下のようにVariablesタブを開き、右上の➕マークをクリックし変数の設定欄を追加する。
設定項目は以下である。
name:jwt_token
動作確認:APIの呼び出し
これで実装は完了です。
「Try API Call」>「Execute Debug」
から動作確認をしてみます。
access_token
が返却されれば成功です。
※失敗する場合はサービスアカウントキーの情報に不備がある可能性が高いです。
CREATE APIの実装
続いて、CREATE APIを作成します。
シナリオの作成方法はアクセストークンの取得APIで説明したため割愛します。
設定するシナリオ定義とScriptブロックは以下になります。
シナリオ定義
項番 | 項目 | 入力値 | 説明 |
---|---|---|---|
1 | category | SampleCrudApi | シナリオが属する分類を指定してください。この情報はシナリオエディタでの階層表示のためのタグとしてのみ使用されます |
2 | workspace | 未入力 | シナリオのワークスペースを設定します |
3 | name | InstanceCreate | シナリオの名前を指定します。ユニークである必要があります。 |
4 | method | POST | APIとして待機するHTTPメソッドを指定します。 |
5 | uri | /sample_crud_api/instances | APIとして待機するHTTPパスを指定します。 |
6 | transaction | 未選択 | トランザクションサービスと連携します。今回は利用しません。 |
7 | routing_auto_generation_mode | 選択 | API Gatewayにルーティングを自動登録するか選択します。 |
scriptブロック
Display Label: get access token
Python code:
faker("getAccessToken")
r = await callout(
path="/sample_crud_api/get_access_token",
method="GET"
)
if r.error:
raise Error(r.code, reason="POST failed")
access_token = json.loads(r.body)["access_token"]
Display Label: insert atom
Python code:
new_instances = await atom.VmInstance.retrieve(instanceName=instance_name)
# インスタンスが存在しなければ新しく作成
if not new_instances:
# VmInstanceクラスのインスタンスを作成する
new_instance = await atom.VmInstance(
instanceName=instance_name,
status="CREATING"
)
# データベースに保存する
await new_instance.save()
else:
new_instance = new_instances[0]
Display Label: create instance
Python code:
faker("postInstance")
r = await callout(
path=f"/GoogleCloud/SampleCrudAPI/{new_instance.projectId}/{new_instance.zone}",
method="POST",
headers={
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
},
body={
"machineType": f"zones/{new_instance.zone}/machineTypes/{new_instance.machineType}",
"name": new_instance.instanceName,
"disks": [
{
"initializeParams": {
"sourceImage": "projects/debian-cloud/global/images/family/debian-12"
},
"boot": True
}
],
"networkInterfaces": [
{
"network": "global/networks/default"
}
],
"shieldedInstanceConfig": {
"enableSecureBoot": False
}
}
)
if r.error:
raise Error(r.code, reason=f"POST failed {new_instance.instanceName}")
Display Label: check status & update status
Python code:
# ポーリング処理
CHECK_INTERVAL = 5
while True:
faker("getInstanceRunning")
r = await callout(
path=f"/GoogleCloud/SampleCrudAPI/{new_instance.projectId}/{new_instance.zone}/{new_instance.instanceName}",
method="GET",
headers={
"Authorization": f"Bearer {access_token}"
},
)
if r.error:
raise Error(r.code, reason=f"Check Status failed {new_instance.instanceName}")
if r.code == 200:
if json.loads(r.body.decode("utf-8"))["status"] in ["RUNNING"]:
gce_id = json.loads(r.body.decode("utf-8"))["id"]
await new_instance.save(status="RUNNING", gceId=gce_id)
break
await asyncio.sleep(CHECK_INTERVAL)
Variablesの設定
Variablesの設定の仕方はアクセストークンの取得の際に記載したので割愛します。
以下をVariablesに設定してください。
name: new_instance
name: access_token
動作確認:APIの呼び出し
Try API Call
を行い、動作確認をしてみましょう。(Content Body
の設定欄はデフォルトでは表示されないので、鉛筆マークから選択してください)
Content Body:
{
"instance_name": "test-instance"
}
Google Cloudのコンソールを確認すると、ComputeEngineにインスタンスが新規作成されているはずです。
READ APIの実装
続いて、READ APIを作成します。
シナリオの作成方法はアクセストークンの取得APIで説明したため割愛します。
設定するシナリオ定義とScriptブロックは以下になります。
シナリオ定義
項番 | 項目 | 入力値 | 説明 |
---|---|---|---|
1 | category | SampleCrudApi | シナリオが属する分類を指定してください。この情報はシナリオエディタでの階層表示のためのタグとしてのみ使用されます |
2 | workspace | 未入力 | シナリオのワークスペースを設定します |
3 | name | InstanceGet | シナリオの名前を指定します。ユニークである必要があります。 |
4 | method | GET | APIとして待機するHTTPメソッドを指定します。 |
5 | uri | /sample_crud_api/instances/{instance_name} | APIとして待機するHTTPパスを指定します。 |
6 | transaction | 未選択 | トランザクションサービスと連携します。今回は利用しません。 |
7 | routing_auto_generation_mode | 選択 | API Gatewayにルーティングを自動登録するか選択します。 |
scriptブロック
Display Label: get instances from atom
Python code:
instance_name = context.request.resources.instance_name
if instance_name:
instances = await atom.VmInstance.retrieve(instanceName=instance_name)
if instances == []:
raise Error(404, reason=f"Instances not exist")
else:
instances = await atom.VmInstance.retrieve()
response = []
for instance in instances:
instance_json = {
"instanceId": instance.instanceId,
"instanceName": instance.instanceName,
"projectId": instance.projectId,
"zone": instance.zone,
"machineType": instance.machineType,
"sourceImage": instance.sourceImage,
"network": instance.network,
"status": instance.status
}
response.append(instance_json)
response_json = json.dumps({"instances": response})
context.session.finish(response_json)
動作確認:APIの呼び出し
Try API Call
を行い、動作確認をしてみましょう。
url resources
の設定欄はデフォルトでは表示されないので、左のCustom API Request
をクリックしてください。
instance_name: test-instance
ATOMのインスタンス情報が取得できるはずです。
UPDATE APIの実装
続いて、UPDATE APIを作成します。
シナリオの作成方法はアクセストークンの取得APIで説明したため割愛します。
設定するシナリオ定義とScriptブロックは以下になります。
シナリオ定義
項番 | 項目 | 入力値 | 説明 |
---|---|---|---|
1 | category | SampleCrudApi | シナリオが属する分類を指定してください。この情報はシナリオエディタでの階層表示のためのタグとしてのみ使用されます |
2 | workspace | 未入力 | シナリオのワークスペースを設定します |
3 | name | InstanceUpdate | シナリオの名前を指定します。ユニークである必要があります。 |
4 | method | PUT | APIとして待機するHTTPメソッドを指定します。 |
5 | uri | /sample_crud_api/instances/{instance_name} | APIとして待機するHTTPパスを指定します。 |
6 | transaction | 未選択 | トランザクションサービスと連携します。今回は利用しません。 |
7 | routing_auto_generation_mode | 選択 | API Gatewayにルーティングを自動登録するか選択します。 |
scriptブロック
Display Label: get access token
Python code:
faker("getAccessToken")
r = await callout(
path="/sample_crud_api/get_access_token",
method="GET"
)
if r.error:
raise Error(r.code, reason="POST failed")
access_token = json.loads(r.body)["access_token"]
Display Label: target machineType
Python code:
instance_name = context.request.resources.instance_name
# Update対象のインスタンスを取得する
update_instances = await atom.VmInstance.retrieve(instanceName=instance_name)
update_instance = update_instances[0]
Display Label: instance stop
Python code:
faker("postInstanceStop")
r = await callout(
path=f"/GoogleCloud/SampleCrudAPI/{update_instance.projectId}/{update_instance.zone}/{update_instance.instanceName}/stop",
method="POST",
headers={
"Authorization": f"Bearer {access_token}"
},
)
if r.error:
raise Error(r.code, reason=f"stop failed")
await update_instance.save(status="STOPPING")
Display Label: check status & update status
Python code:
# ポーリング処理
CHECK_INTERVAL = 5
while True:
faker("getInstanceTerminated")
r = await callout(
path=f"/GoogleCloud/SampleCrudAPI/{update_instance.projectId}/{update_instance.zone}/{update_instance.instanceName}",
method="GET",
headers={
"Authorization": f"Bearer {access_token}"
},
)
if r.error:
raise Error(r.code, reason=f"get failed")
if r.code == 200:
if json.loads(r.body.decode("utf-8"))["status"] in ["TERMINATED"]:
await update_instance.save(status="STOPPED")
break
else:
await update_instance.save(status="FAILED")
break
# 同期のsleepは不可
await asyncio.sleep(CHECK_INTERVAL)
Display Label: post machineType
Python code:
faker("postInstanceMachineType")
r = await callout(
path=f"/GoogleCloud/SampleCrudAPI/{update_instance.projectId}/{update_instance.zone}/{update_instance.instanceName}/setMachineType",
method="POST",
headers={
"Authorization": f"Bearer {access_token}"
},
body={
"machineType": f"zones/{update_instance.zone}/machineTypes/{machine_type}"
}
)
if r.error:
raise Error(r.code, reason=f"post failed")
Display Label: check changing machineType
Python code:
import os
# ポーリング処理
CHECK_INTERVAL = 5
while True:
faker("getInstanceMachineType")
r = await callout(
path=f"/GoogleCloud/SampleCrudAPI/{update_instance.projectId}/{update_instance.zone}/{update_instance.instanceName}",
method="GET",
headers={
"Authorization": f"Bearer {access_token}"
},
)
if r.error:
raise Error(r.code, reason=f"get failed {update_instance.instanceName}")
if r.code == 200:
# インスタンスのマシンタイプを取得する
instance_machine_type = os.path.basename(json.loads(r.body.decode("utf-8"))["machineType"])
# インスタンスのマシンタイプと今回変更したいマシンタイプが一致した場合、更新は終わっているためポーリングを終了する
if instance_machine_type == machine_type:
break
await asyncio.sleep(CHECK_INTERVAL)
Display Label: instance start
Python code:
faker("postInstanceStart")
r = await callout(
path=f"/GoogleCloud/SampleCrudAPI/{update_instance.projectId}/{update_instance.zone}/{update_instance.instanceName}/start",
method="POST",
headers={
"Authorization": f"Bearer {access_token}"
},
)
if r.error:
raise Error(r.code, reason=f"start failed")
await update_instance.save(status="STARTING")
Display Label: check status & update status
Python code:
# ポーリング処理
CHECK_INTERVAL = 5
while True:
faker("getInstanceRunning")
r = await callout(
path=f"/GoogleCloud/SampleCrudAPI/{update_instance.projectId}/{update_instance.zone}/{update_instance.instanceName}",
method="GET",
headers={
"Authorization": f"Bearer {access_token}"
},
)
if r.error:
raise Error(r.code, reason=f"get failed")
if r.code == 200:
if json.loads(r.body.decode("utf-8"))["status"] in ["RUNNING"]:
await update_instance.save(status="RUNNING")
break
else:
await update_instance.save(status="FAILED")
break
# 同期のsleepは不可
await asyncio.sleep(CHECK_INTERVAL)
Display Label: update machineType
Python code:
# machineTypeを置き換える
await update_instance.save(machineType=machine_type)
Variablesの設定
name: update_instance
name: access_token
Endpointの設定
ScenarioにはTimeout
設定があり、Scenario内の処理に時間をようしている場合、指定時間経過した時に自動でタイムアウトします。
デフォルト値は60sですが、GoogleCloudのVMのMachineTypeの変更が反映されるまでに時間がかかるため、ポーリング処理中にタイムアウトしてしまいます。
そこで以下のようにEndpointのタブからconnect_timeout
とrequest_timeout
を600s
に変更してください。
動作確認:APIの呼び出し
Try API Call
を行い、動作確認をしてみましょう。
url resourses
にはtest-instance
を入力し、Content Body
には以下を入力しましょう。
{
"machine_type": "e2-small"
}
Google Cloudのコンソールを開いて、ComputeEngineのインスタンスを確認するとMachineTypeが更新されているはずです。
DELETE APIの実装
続いて、DELETE APIを作成します。
シナリオの作成方法はアクセストークンの取得APIで説明したため割愛します。
設定するシナリオ定義とScriptブロックは以下になります。
シナリオ定義
項番 | 項目 | 入力値 | 説明 |
---|---|---|---|
1 | category | SampleCrudApi | シナリオが属する分類を指定してください。この情報はシナリオエディタでの階層表示のためのタグとしてのみ使用されます |
2 | workspace | 未入力 | シナリオのワークスペースを設定します |
3 | name | InstanceDelete | シナリオの名前を指定します。ユニークである必要があります。 |
4 | method | DELETE | APIとして待機するHTTPメソッドを指定します。 |
5 | uri | /sample_crud_api/instances/{instance_name} |
APIとして待機するHTTPパスを指定します。 |
6 | transaction | 未選択 | トランザクションサービスと連携します。今回は利用しません。 |
7 | routing_auto_generation_mode | 選択 | API Gatewayにルーティングを自動登録するか選択します。 |
scriptブロック
Display Label: get access token
Python code:
faker("getAccessToken")
r = await callout(
path="/sample_crud_api/get_access_token",
method="GET"
)
if r.error:
raise Error(r.code, reason="POST failed")
access_token = json.loads(r.body)["access_token"]
Display Label: target atom
Python code:
instance_name = context.request.resources.instance_name
delete_instances = await atom.VmInstance.retrieve(instanceName=instance_name)
delete_instance = delete_instances[0]
Display Label: delete instance
Python code:
faker("deleteInstance")
r = await callout(
path=f"/GoogleCloud/SampleCrudAPI/{delete_instance.projectId}/{delete_instance.zone}/{delete_instance.instanceName}",
method="DELETE",
headers={
"Authorization": f"Bearer {access_token}"
},
)
if r.error:
raise Error(r.code, reason=f"DELETE failed {delete_instance.instanceName}")
# DELETINGステータスに変更
await delete_instance.save(status="DELETING")
Display Label: check status
Python code:
# ポーリング処理
CHECK_INTERVAL = 5
while True:
faker("getInstance")
r = await callout(
path=f"/GoogleCloud/SampleCrudAPI/{delete_instance.projectId}/{delete_instance.zone}/{delete_instance.instanceName}",
method="GET",
headers={
"Authorization": f"Bearer {access_token}"
},
)
if r.code == 404:
await delete_instance.save(status="DELETED")
break
elif r.code == 200:
pass
else:
await delete_instance.save(status="FAILED")
break
await asyncio.sleep(CHECK_INTERVAL)
Variableの設定
name: delete_instance
name: access_token
Endpointの設定
UPDATE API同様にGoogle Cloudのインスタンスを削除する処理は時間を要するため、Endpointのタブからconnect_timeout
とrequest_timeout
を600s
に変更してください。
動作確認:APIの呼び出し
Try API Call
を行い、動作確認をしてみましょう。
Google Cloudのコンソールにて確認すると、ComputeEngineにインスタンスが新規作成されているはずです。
トランザクションの実装
より安全なリソース管理のために
これまでのSample CRUD APIの実装では、基本的なリソースの作成、読み取り、更新、削除が可能になりました。しかし、実際の運用環境では、APIの実行中に予期せぬエラーが発生する可能性があります。特に、複数の処理が連携する更新処理(Update API)では、一部の処理が失敗した場合にシステムの状態が不整合になるリスクがあります。
例えば、Update APIではCompute Engineインスタンスの設定を変更する際に、以下の手順を実行していました。
- インスタンスを停止
- マシンタイプを変更
- インスタンスを再起動
ここで、2番目の「インスタンスのマシンタイプを変更する」処理で、Google Cloudから応答が返ってこなかったらどうなるでしょうか?
応答がないため、Qmonus SDK側では、マシンタイプの変更が成功したのか、失敗したのかを判断できません。
そうなると、Google Cloud上のインスタンスの状態と、Qmonus SDKが管理しているインスタンスの状態が食い違ってしまう可能性があります。
これでは、安全にリソース管理ができているとは言えません。
このような問題を解決し、システムの状態を常に一貫性のある状態に保つために、トランザクションの概念が重要になります。
Qmonus SDKでは、冪等性を担保するためのトランザクション機能が組み込まれています。
Note
トランザクションとは、一連の処理を「ひとまとまりの処理」として扱う仕組みです。
トランザクションを使うと、一連の処理は、すべて成功するか、またはすべて失敗するかのどちらかになります。
これにより、Sample CRUD APIにトランザクションを導入することで、冪等性(同じ操作を何度実行しても、結果が常に同じになる)を担保することができます。
Google Cloudのレスポンスが帰ってこない場合であっても、トランザクション機能によって安全にロールバックし、インスタンスの設定を更新前の状態に戻すことで、システムの状態を矛盾なく保つことができます。
この章では、より安全なリソース管理を目指して、1章で作成したSample CRUD APIにトランザクション機能を実装していきます。
トランザクションの実装方針
Sample CRUD APIでは以下のようにトランザクションを実装することにします。
CREATE:
リソースの作成中にエラーが発生した場合、システムの状態を矛盾なく保つために、処理全体をロールバックします。
READ:
読み取り操作はシステムの状態を変更しないため、トランザクションは実装しません。
UPDATE:
リソースの更新中にエラーが発生した場合、システムの状態を矛盾なく保つために、処理全体をロールバックします。
DELETE:
リソースの削除は、一度実行すると元に戻せない操作です。そのため、トランザクションは実装しますが、エラーが発生した場合でもロールバックは行わず、ロールフォワードのみを許可します。
それではまずは、CREATE APIからトランザクション機能を実装していきます。
CREATE APIのトランザクション実装
トランザクションのステータス
トランザクションを実装する前に、Qmonus SDKのトランザクションのステータス管理を理解しておく必要があります。
Qmonus SDKではトランザクションは、次の状態マシンによって管理されています。
これを今回実装したCREATE APIに当てはめてみます。
まずCREATE APIが呼ばれるとステータスはProcessing
となります。
処理が正常に終了するとComplete
、途中で処理が失敗するとAborted
になります。
例えば、CREATE APIの途中でGoogle Cloud APIからレスポンスステータスコード500
が返却されるなどしてraise Errorとなった場合などはAborted
となります。
Aborted
になった際に、キャンセル処理を有効にしていた場合はCancelling
となり、キャンセル処理が実行されます。
キャンセル処理はシナリオの各トイブロックに対して設定でき、そのトイブロックの状態を実行前に戻すように実装します。
Aborted
になったトイブロックから、実行した順番をさかのぼるようにキャンセル処理を実行していき、最終的にトランザクションの実行前の状態に戻ります。
こうして正常にCancelling
が終了した場合はCancelled
となり、トランザクションは終了します。(いわゆるロールバックの処理です)
なお、Cancelling
の途中でふたたびエラーになる場合はAbortedに戻ります。
トランザクションのステータスはポータルから適宜参照できるため、Aborted
になったトランザクションは必要に応じてユーザーが個別に対応することになります。
以上がロールバックの場合のトランザクションのステータス遷移になります。 これを踏まえたうえで、実際にCREATE APIにロールバック処理を実装してみます。
CREATE APIでトランザクションを有効にする
まず、CREATE APIのTransaction
タブを開き、以下のように設定します。
(項目が見当たらない場合は適宜鉛筆マークから項目を表示させたうえで設定してください)
なお、それぞれ以下の設定となります。
Transaction Mode
項目 | 設定値 | 説明 |
---|---|---|
enable | True | トランザクション機能を有効にする。 |
Transaction Properties
項目 | 設定値 | 説明 |
---|---|---|
async | True | トランザクションが終了したタイミングでトランザクションからシナリオにcallbackを返すようになります。 |
auto_rollback | True | トランザクション内でエラーが発生した場合、自動的にロールバックを実行するようになります。 |
auto_begin | True | トランザクションが自動的に開始されるようになります。トランザクション開始時にレスポンスステータスコード202を返します。 |
auto_response | True | トランザクションの結果が自動的にレスポンスのBodyとして返されるようになります。 |
各トイブロックに対してCancellationを実装する
続いて、キャンセル処理を実装していきます。
キャンセル処理は各トイブロックごとに許可するかしないかを設定します。
CREATE APIでは、以下のように設定します。
トイブロック名 | Cancellable |
---|---|
get access token | 未設定 |
insert atom | True |
create instance | True |
check status & update status | True |
上記のトイブロックを選択し、Parameters
からCancellation
をチェックし、Cancellable
をTrue
にします。
また、Cancellation
のRollback setting
をチェックし、Python code
を入力します。
Python code
はそれぞれ以下を入力します。
トイブロック: get access token
Python code(Rollback setting):
# VmInstanceクラスのインスタンスを削除
instances = await atom.VmInstance.retrieve(instanceName=instance_name)
instance = instances[0]
await instance.destroy()
<方針>
このトイブロックでは、新規のインスタンスをデータベースに保存しています。
そのため、キャンセル処理ではこのトイブロックの実行前の状態に戻したいので、作成したインスタンスを削除する処理となっています。
<処理の流れ>
まず新規作成したインスタンス名で検索し、該当のインスタンスを取得します。
そのあと、このインスタンスを destroyにより削除して、このトイブロック実行前の状態に戻します。
トイブロック: create instance
Python code(Rollback setting):
r = await callout(
path=f"/GoogleCloud/SampleCrudAPI/{new_instance.projectId}/{new_instance.zone}/{new_instance.instanceName}",
method="GET",
headers={
"Authorization": f"Bearer {access_token}"
},
)
# VMが作成されていない場合はcallbackで行うことはないためreturnする
if r.code == 404:
return
# VMが作成されている場合はcallbackでこのVMを削除する必要がある
if r.code == 200:
# ステータスをDELETINGにする
await new_instance.save(status="DELETING")
# DELETE
r = await callout(
path=f"/GoogleCloud/SampleCrudAPI/{new_instance.projectId}/{new_instance.zone}/{new_instance.instanceName}",
method="DELETE",
headers={
"Authorization": f"Bearer {access_token}"
},
)
if r.error:
raise Error(r.code, reason=f"callback delete failed {new_instance.instanceName}")
<方針>
このトイブロックでは、Google Cloud APIを呼び出してインスタンスを新規に作成しています。
そのため、キャンセル処理ではこのトイブロックの実行前の状態に戻したいので、インスタンスが新規に作成されている場合はそれを削除するようにします。
<処理の流れ>
まずGoogle Cloud側にインスタンスが新規に作成されているかどうかがわからないため、Google Cloudのインスタンスの状態をGETにより確認します。
その際に、ステータス404が返る場合は、インスタンスが作成されていないということなので何もせず終了します。
200が返る場合は、インスタンスが作成されているということなので、DELETEにより該当のインスタンスを削除します。
トイブロック: check status & update status
Python code(Rollback setting):
# GET
r = await callout(
path=f"/GoogleCloud/SampleCrudAPI/{new_instance.projectId}/{new_instance.zone}/{new_instance.instanceName}",
method="GET",
headers={
"Authorization": f"Bearer {access_token}"
},
)
if r.error:
raise Error(r.code, reason=f"callback get failed {new_instance.instanceName}")
# 200 かつ RUNNINGでない場合はタイムアウトエラーを返す
if r.code == 200:
if json.loads(r.body.decode("utf-8"))["status"] != "RUNNING":
raise Error(r.code, reason=f"callback get failed {new_instance.instanceName}")
# 200 以外の場合はエラーを返す
if r.code != 200:
raise Error(r.code, reason=f"callback get failed {new_instance.instanceName}")
# ステータスをCREATINGに戻す
await new_instance.save(status="CREATING")
<方針>
このトイブロックでは、GETをポーリングしてインスタンスが無事に作成されたかを確認し、作成されている場合はSDK側のインスタンスのステータスをRUNNINGに変更しています。
そのため、キャンセル処理ではこのトイブロックの実行前の状態に戻したいので、インスタンスがGoogle Cloud側に作成されている場合はSDK側のインスタンスのステータスをCREATINGに戻します。
<処理の流れ>
まずGoogle Cloud側にインスタンスが新規に作成されているかどうかがわからないため、Google Cloudのインスタンスの状態をGETにより確認します。
その際に、ステータス200かつRUNNINGでない場合、タイムアウトエラーしていたということなのでタイムアウトエラーとします。
200以外が返る場合は、予期しないことが起こっているためエラーとします。
200が返る場合は、インスタンスが作成されているということなので、SDK側のインスタンスのステータスをCREATINGに戻します。
以上により、CREATE APIにおけるトランザクションを実装できました。 トランザクションの動作確認は「3.ユニットテストの実装」で行います。
READ APIのトランザクション実装
READ APIは読み取り操作はシステムの状態を変更しないため、トランザクションは実装しません。
UPDATE APIのトランザクション実装
UPDATE APIでトランザクションを有効にする
CREATE API同様、以下のように設定します。
各トイブロックに対してCancellationを実装する
続いて、キャンセル処理を実装していきます。
キャンセル処理は各トイブロックごとに許可するかしないかを設定します。
UPDATE APIでは、以下のように設定します。
トイブロック名 | Cancellable |
---|---|
get access token | 未設定 |
target machineType | True |
instance stop | True |
check status & update status | True |
post machineType | True |
check changing machineType | True |
instance start | True |
check status & update status | True |
update machineType | False |
CREATE APIを参考に、以下のようにそれぞれのトイブロックに対してCancellation
のPython code
を入力します。
トイブロック: get access token
Python code(Rollback setting): 未設定
トイブロック: target machineType
Python code(Rollback setting):
# 未設定
<方針>
このトイブロックでは、Updateの対象となるインスタンスをインスタンス名から取得しています。
そのため、このトイブロックでは参照しかしていないため、CancellableはTrueとしてロールバック可にしていますが、キャンセル処理についてはなしとしています。
トイブロック: instance stop
Python code(Rollback setting):
# GET: インスタンスの状態を確認する
faker("getInstanceTerminated")
r = await callout(
path=f"/GoogleCloud/SampleCrudAPI/{update_instance.projectId}/{update_instance.zone}/{update_instance.instanceName}",
method="GET",
headers={
"Authorization": f"Bearer {access_token}"
},
)
if r.error:
raise Error(r.code, reason=f"callback get failed {update_instance.instanceName}")
if r.code == 200:
# RUNNING:何もせずreturn
if json.loads(r.body.decode("utf-8"))["status"] in ["RUNNING"]:
return
# TERMINATED:このVMを再起動する
if json.loads(r.body.decode("utf-8"))["status"] in ["TERMINATED"]:
# ステータスをSTARTINGにする
await update_instance.save(status="STARTING")
# STARTする
faker("postInstanceStart")
r = await callout(
path=f"/GoogleCloud/SampleCrudAPI/{update_instance.projectId}/{update_instance.zone}/{update_instance.instanceName}/start",
method="POST",
headers={
"Authorization": f"Bearer {access_token}"
},
)
# STARTできていることをポーリングで確認する
CHECK_INTERVAL = 5
while True:
faker("getInstanceRunning")
r = await callout(
path=f"/GoogleCloud/SampleCrudAPI/{update_instance.projectId}/{update_instance.zone}/{update_instance.instanceName}",
method="GET",
headers={
"Authorization": f"Bearer {access_token}"
},
)
if r.error:
raise Error(r.code, reason=f"callback get failed {update_instance.instanceName}")
# 200かつステータスがRUNNINGの場合:return
if (r.code == 200) and (json.loads(r.body.decode("utf-8"))["status"] in ["RUNNING"]):
await update_instance.save(status="RUNNING")
return
await asyncio.sleep(CHECK_INTERVAL)
<方針>
このトイブロックでは、インスタンスをアップデートするため、対象のインスタンスを停止して、SDK側のインスタンスのステータスをRUNNINGからSTOPPINGに変更しています。
そのため、キャンセル処理では停止前の状態に戻したいので、インスタンスの状態を確認し、停止していた場合は再起動してSDK側のインスタンスのステータスをRUNNINGに変更します。
<処理の流れ>
まずGoogle Cloud側のインスタンスのステータスがわからないのでGETで確認します。
その際に、ステータス200かつRUNNINGが返る場合は、すでに起動状態にあるので何もせず終了します。
ステータス200かつTERMINATEDが返る場合は、停止状態にあるのでGoogle Cloud APIにより再起動をし、ちゃんと再起動できているかをポーリングで確認しています。起動できていた場合、SDK側のインスタンスのステータスをRUNNINGに戻します。
トイブロック: check status & update status
Python code(Rollback setting):
# GET: インスタンスの状態を確認する
faker("getInstanceTerminated")
r = await callout(
path=f"/GoogleCloud/SampleCrudAPI/{update_instance.projectId}/{update_instance.zone}/{update_instance.instanceName}",
method="GET",
headers={
"Authorization": f"Bearer {access_token}"
},
)
if r.error:
raise Error(r.code, reason=f"callback get failed {update_instance.instanceName}")
if r.code == 200:
if json.loads(r.body.decode("utf-8"))["status"] in ["TERMINATED"]:
# 200かつTERMINATEDの場合:ステータスをSTOPPINGに変更
await update_instance.save(status="STOPPING")
return
else:
# 200かつTERMINATEDでない場合:タイムアウトエラー
raise Error(r.code, reason=f"callback get failed {update_instance.instanceName}")
# 上記以外の場合:エラー
raise Error(r.code, reason=f"callback get failed {update_instance.instanceName}")
<方針>
このトイブロックでは、GETをポーリングしてインスタンスが無事に停止したかを確認し、停止している場合はSDK側のインスタンスのステータスをSTOPPINGからSTOPPEDに変更しています。
そのため、キャンセル処理ではこのトイブロックの実行前の状態に戻したいので、GETでインスタンスの状態を確認し、インスタンスが停止している場合はSDK側のインスタンスのステータスをSTOPPINGに戻します。
<処理の流れ>
まずGoogle Cloud側のインスタンスのステータスがわからないのでGETで確認します。
200かつTERMINATEDの場合は、インスタンスが停止されているため、ステータスをSTOPPINGに変更します。
200かつTERMINATEDでない場合はタイムアウトエラーが考えられるのでタイムアウトエラーとします。
それ以外の場合は、予期しないことが起こっているためエラーとします。
トイブロック: post machineType
Python code(Rollback setting):
import os
# GET: インスタンスのマシンタイプを確認する
faker("getInstanceMachineType")
r = await callout(
path=f"/GoogleCloud/SampleCrudAPI/{update_instance.projectId}/{update_instance.zone}/{update_instance.instanceName}",
method="GET",
headers={
"Authorization": f"Bearer {access_token}"
},
)
if r.error:
raise Error(r.code, reason=f"callback get failed {update_instance.instanceName}")
if r.code == 200:
# インスタンスのマシンタイプを取得する
instance_machine_type = os.path.basename(json.loads(r.body.decode("utf-8"))["machineType"])
# マシンタイプが更新されていない場合:何もせずreturn
if instance_machine_type == update_instance.machineType:
return
# マシンタイプが更新されていた場合:POSTでマシンタイプを元に戻す
elif instance_machine_type == machine_type:
# POST
r = await callout(
path=f"/GoogleCloud/SampleCrudAPI/{update_instance.projectId}/{update_instance.zone}/{update_instance.instanceName}/setMachineType",
method="POST",
headers={
"Authorization": f"Bearer {access_token}"
},
body={
"machineType": f"zones/{update_instance.zone}/machineTypes/{update_instance.machineType}"
}
)
if r.error:
raise Error(r.code, reason=f"post failed")
# GET:ポーリングでマシンタイプの更新を確認する
CHECK_INTERVAL = 5
while True:
r = await callout(
path=f"/GoogleCloud/SampleCrudAPI/{update_instance.projectId}/{update_instance.zone}/{update_instance.instanceName}",
method="GET",
headers={
"Authorization": f"Bearer {access_token}"
},
)
if r.error:
raise Error(r.code, reason=f"callback get failed {update_instance.instanceName}")
# 200かつマシンタイプが更新されていた:終了
if r.code == 200:
if os.path.basename(json.loads(r.body.decode("utf-8"))["machineType"]) == update_instance.machineType:
return
else:
raise Error(r.code, reason=f"callback get failed {update_instance.instanceName}")
await asyncio.sleep(CHECK_INTERVAL)
raise Error(r.code, reason=f"callback get failed {update_instance.instanceName}")
<方針>
このトイブロックでは、Google Cloud APIを呼び出してマシンタイプを変更しています。
そのため、キャンセル処理ではこのトイブロックの実行前の状態に戻したいので、GETでインスタンスの状態を確認し、インスタンスのマシンタイプが変更されている場合はそれを戻すようにGoogle Cloud APIを呼び出します。
<処理の流れ>
まずGoogle Cloud側のインスタンスのマシンタイプがわからないのでGETで確認します。
その結果、マシンタイプが更新されていなかったら何もせず終了します。
マシンタイプが更新されていた場合は、POSTでマシンタイプを元に戻します。
その後、マシンタイプが無事に元に戻ったかをGETをポーリングして確認します。
200かつマシンタイプが更新されていた場合は終了し、それ以外の場合はエラーを返します。
トイブロック: check changing machineType
Python code(Rollback setting):
# 未設定
<方針>
このトイブロックでは、マシンタイプが無事に更新されたかどうかをGETでポーリングで確認します。
そのため、このトイブロックでは参照しかしていないため、CancellableはTrueとしてロールバック可にしていますが、キャンセル処理についてはなしとしています。
トイブロック: instance start
Python code(Rollback setting):
# GET: インスタンスの状態を確認する
r = await callout(
path=f"/GoogleCloud/SampleCrudAPI/{update_instance.projectId}/{update_instance.zone}/{update_instance.instanceName}",
method="GET",
headers={
"Authorization": f"Bearer {access_token}"
},
)
if r.error:
raise Error(r.code, reason=f"callback get failed {update_instance.instanceName}")
if r.code == 200:
# TERMINATED:何もせずreturn
if json.loads(r.body.decode("utf-8"))["status"] in ["TERMINATED"]:
return
# RUNNING:このVMを停止する
if json.loads(r.body.decode("utf-8"))["status"] in ["RUNNING"]:
# ステータスをSTOPPINGにする
await update_instance.save(status="STOPPING")
# STOPする
r = await callout(
path=f"/GoogleCloud/SampleCrudAPI/{update_instance.projectId}/{update_instance.zone}/{update_instance.instanceName}/stop",
method="POST",
headers={
"Authorization": f"Bearer {access_token}"
},
)
# STOPできていることをポーリングで確認する
CHECK_INTERVAL = 5
while True:
r = await callout(
path=f"/GoogleCloud/SampleCrudAPI/{update_instance.projectId}/{update_instance.zone}/{update_instance.instanceName}",
method="GET",
headers={
"Authorization": f"Bearer {access_token}"
},
)
if r.error:
raise Error(r.code, reason=f"callback get failed {update_instance.instanceName}")
# 200かつステータスがTERMINATEDの場合:return
if (r.code == 200) and (json.loads(r.body.decode("utf-8"))["status"] in ["TERMINATED"]):
await update_instance.save(status="STOPPED")
return
await asyncio.sleep(CHECK_INTERVAL)
<方針>
このトイブロックでは、停止していた対象のインスタンスを再起動して、SDK側のインスタンスのステータスをSTOPPEDからSTARTINGに変更しています。
そのため、キャンセル処理では停止前の状態に戻したいので、インスタンスの状態を確認し、再起動していた場合は停止してSDK側のインスタンスのステータスをSTOPPEDに変更します。
<処理の流れ>
まずGoogle Cloud側のインスタンスのステータスがわからないのでGETで確認します。
その際に、ステータス200かつTERMINATEDが返る場合は、すでに停止状態にあるので何もせず終了します。
ステータス200かつRUNNINGが返る場合は、起動状態にあるのでGoogle Cloud APIにより停止をし、ちゃんと停止できているかをポーリングで確認しています。停止できていた場合、SDK側のインスタンスのステータスをSTOPPED
に戻します。
トイブロック: check status & update status
Python code(Rollback setting):
# GET: インスタンスの状態を確認する
r = await callout(
path=f"/GoogleCloud/SampleCrudAPI/{update_instance.projectId}/{update_instance.zone}/{update_instance.instanceName}",
method="GET",
headers={
"Authorization": f"Bearer {access_token}"
},
)
if r.error:
raise Error(r.code, reason=f"callback get failed {update_instance.instanceName}")
if r.code == 200:
if json.loads(r.body.decode("utf-8"))["status"] in ["RUNNING"]:
# 200かつRUNNINGの場合:ステータスをSTARTINGに変更
await update_instance.save(status="STARTING")
return
else:
# 200かつRUNNINGでない場合:タイムアウトエラー
raise Error(r.code, reason=f"callback get failed {update_instance.instanceName}")
# 上記以外の場合:エラー
raise Error(r.code, reason=f"callback get failed {update_instance.instanceName}")
<方針>
このトイブロックでは、GETをポーリングしてインスタンスが無事に起動したかを確認し、起動している場合はSDK側のインスタンスのステータスをSTARTINGからRUNNINGに変更しています。
そのため、キャンセル処理ではこのトイブロックの実行前の状態に戻したいので、GETでインスタンスの状態を確認し、インスタンスが起動している場合はSDK側のインスタンスのステータスをSTARTINGに戻します。
<処理の流れ>
まずGoogle Cloud側のインスタンスのステータスがわからないのでGETで確認します。
200かつRUNNINGの場合は、インスタンスが起動されているため、ステータスをSTATINGに変更します。
200かつRUNNINGでない場合はタイムアウトエラーが考えられるのでタイムアウトエラーとします。
それ以外の場合は、予期しないことが起こっているためエラーとします。
トイブロック: update machineType
Python code(Rollback setting): False
<方針>
これより前のトイブロックでGoogle Cloud側のインスタンスのマシンタイプを変更したため、このトイブロックでは、SDKのインスタンスのマシンタイプを更新する処理を行っている。
この段階から、ロールバックでマシンタイプを元に戻すのは許可しないとし、CancellableをFalseとしている。
以上により、UPDATE APIにおけるトランザクションを実装できました。
トランザクションの動作確認は「3.ユニットテストの実装」で行います。
DELETE APIのトランザクション実装
DELETE APIでトランザクションを有効にする
CREATE API同様に設定します。
各トイブロックに対してCancellationを実装する
DELETE APIでは、以下のように設定します。
トイブロック名 | Cancellable |
---|---|
get access token | False |
target atom | False |
delete instance | False |
check status | False |
DELETE APIで扱うリソースの削除は、一度実行すると元に戻せない操作です。
そのため、トランザクションは実装しますが、エラーが発生した場合でもロールバックは行わず、ロールフォワードのみを許可します。
したがって、トランザクションは設定しますが、Cancellable
はすべてのトイブロックにおいてFalseとし、ロールバックを禁止しています。
以上により、DELETE APIにおけるトランザクションを実装できました。 トランザクションの動作確認は「3.ユニットテストの実装」で行います。
ユニットテストの実装
ここまでで、Sample CRUD APIをトランザクション含め実装することができました。
ここからは実装したSample CRUD APIのユニットテストを実装していこうと思います。
テストはこちらから作成することができます。
ユニットテストは以下の順に作成します。
-
Faker
テスト実行時に、外部サービスとの実際の通信を行わずに、あらかじめ定義した疑似動作(フェイク関数)を実行して応答を模擬する仕組みです。 -
Illusion
Fakerで定義されたフェイク関数群を、テストシーンに適用するための設定・割り当ての仕組みです。これにより、特定のテスト実行時に外部環境の動作を模擬できます。 -
Test Case
一つのテストシナリオを構成する単位で、入力条件や期待する出力、検証(assert)処理などを記述し、個別の機能やプラグインの動作をテストします。 -
Test Suite
複数のTest Caseをまとめた集合体で、関連するテストケースを一括して実行し、システム全体や複数機能間の連携動作を検証するために使用されます。
Fakerの実装
今回作成するFaker
今回のSample CRUD APIでは、以下のFakerを用意します。
faker名 | 使用するCRUD API | 想定する用途 |
---|---|---|
getAccessToken | CREATE UPDATE DELETE |
疑似的にGETしてGoogle Cloud APIのアクセストークンを取得した際の挙動をする |
getInstance | CREATE DELETE |
疑似的にGETしてインスタンスを取得した際の挙動をする |
getInstanceRunning | CREATE UPDATE |
疑似的にGETしてインスタンスを取得した際の挙動をする 成功時にRUNNINGステータスを返す |
getInstanceTerminated | UPDATE | 疑似的にGETしてインスタンスを取得した際の挙動をする 成功時にTERMINATEDステータスを返す |
getInstanceMachineType | UPDATE | 疑似的にGETしてインスタンスを取得した際の挙動をする MachineTypeにe2-microもしくはe2-smallを返す |
postInstance | CREATE | 疑似的にPOSTしてインスタンスを作成した際の挙動をする |
postInstanceStop | UPDATE | 疑似的にPOSTしてインスタンスを停止した際の挙動をする |
postInstanceStart | UPDATE | 疑似的にPOSTしてインスタンスを再起動した際の挙動をする |
postInstanceMachineType | UPDATE | 疑似的にPOSTしてMachineTypeを更新した際の挙動をする |
deleteInstance | DELETE | 疑似的にDELETEしてインスタンスを削除した際の挙動をする |
Note
どうしてFakerが必要なのか:
今回のSample CRUD APIに対しテストコードを実装しようとすると、まず悩むところとしてGoogle Cloud APIの呼び出しをどうするかがあると思います。そのまま各APIをテストで実行してしまうと、テストのたびにGoogle Cloudにリクエストが飛んでしまいます。
この問題に対して、Fakerを使うとこれを解決することができます。
Fakerを定義しcallout関数に対しておいてあげることで、そのcalloutでは実際にAPIを呼び出すことなく、Fakerで定義した疑似動作を行うことになります。これにより、外部にリクエストを飛ばさずにテストの実行を実現することができます。
Fakerの新規作成
それでは一つずつFakerを作成していきます。
[Unit Test as a Service]>[Faker]>[Create New Faker]
からFakerを作成することができます。
作成後、Fakerタブからふるまいを定義することができます。
以下のFakerを作成します。
name: getAccessToken
category: SampleCrudApi
fakes:
async def Success(*args, **kwargs):
return FakeHttpResponse(
200,
body=json.dumps({"access_token": "xxxxx"}).encode("utf-8")
)
async def InternalError(*args, **kwargs):
return FakeHttpResponse(500)
async def NotFound(*args, **kwargs):
return FakeHttpResponse(404)
name: getInstance
category: SampleCrudApi
fakes:
async def Success(*args, **kwargs):
return FakeHttpResponse(200)
async def Running(*args, **kwargs):
return FakeHttpResponse(
200,
body=json.dumps({"status": "RUNNING", "id": "111"}).encode("utf-8")
)
async def Terminated(*args, **kwargs):
return FakeHttpResponse(
200,
body=json.dumps({"status": "TERMINATED", "id": "111"}).encode("utf-8")
)
async def e2micro(*args, **kwargs):
return FakeHttpResponse(
200,
# projectId, zoneには各自がデフォルトで設定している値に書き換えてください
body=json.dumps({"machineType": "https://www.googleapis.com/compute/v1/projects/<projectId>/zones/<zone>/machineTypes/e2-micro"}).encode("utf-8")
)
async def e2small(*args, **kwargs):
return FakeHttpResponse(
200,
# projectId, zoneには各自がデフォルトで設定している値に書き換えてください
body=json.dumps({"machineType": "https://www.googleapis.com/compute/v1/projects/<projectId>/zones/<zone>/machineTypes/e2-small"}).encode("utf-8")
)
async def InternalError(*args, **kwargs):
return FakeHttpResponse(500)
async def NotFound(*args, **kwargs):
return FakeHttpResponse(404)
name: getInstanceRunning
category: SampleCrudApi
fakes:
async def Success(*args, **kwargs):
return FakeHttpResponse(200)
async def Running(*args, **kwargs):
return FakeHttpResponse(
200,
body=json.dumps({"status": "RUNNING", "id": "111"}).encode("utf-8")
)
async def InternalError(*args, **kwargs):
return FakeHttpResponse(500)
async def NotFound(*args, **kwargs):
return FakeHttpResponse(404)
name: getInstanceTerminated
category: SampleCrudApi
fakes:
async def Success(*args, **kwargs):
return FakeHttpResponse(200)
async def Terminated(*args, **kwargs):
return FakeHttpResponse(
200,
body=json.dumps({"status": "TERMINATED", "id": "111"}).encode("utf-8")
)
async def InternalError(*args, **kwargs):
return FakeHttpResponse(500)
async def NotFound(*args, **kwargs):
return FakeHttpResponse(404)
name: getInstanceMachineType
category: SampleCrudApi
fakes:
async def Success(*args, **kwargs):
return FakeHttpResponse(200)
async def e2micro(*args, **kwargs):
return FakeHttpResponse(
200,
# projectId, zoneには各自がデフォルトで設定している値に書き換えてください
body=json.dumps({"machineType": "https://www.googleapis.com/compute/v1/projects/<projectId>/zones/<zone>/machineTypes/e2-micro"}).encode("utf-8")
)
async def e2small(*args, **kwargs):
return FakeHttpResponse(
200,
# projectId, zoneには各自がデフォルトで設定している値に書き換えてください
body=json.dumps({"machineType": "https://www.googleapis.com/compute/v1/projects/<projectId>/zones/<zone>/machineTypes/e2-small"}).encode("utf-8")
)
async def InternalError(*args, **kwargs):
return FakeHttpResponse(500)
async def NotFound(*args, **kwargs):
return FakeHttpResponse(404)
name: postInstance
category: SampleCrudApi
fakes:
async def Success(*args, **kwargs):
return FakeHttpResponse(200)
async def InternalError(*args, **kwargs):
return FakeHttpResponse(500)
async def NotFound(*args, **kwargs):
return FakeHttpResponse(404)
name: postInstanceStop
category: SampleCrudApi
fakes:
async def Success(*args, **kwargs):
return FakeHttpResponse(200)
async def InternalError(*args, **kwargs):
return FakeHttpResponse(500)
async def NotFound(*args, **kwargs):
return FakeHttpResponse(404)
name: postInstanceStart
category: SampleCrudApi
fakes:
async def Success(*args, **kwargs):
return FakeHttpResponse(200)
async def InternalError(*args, **kwargs):
return FakeHttpResponse(500)
async def NotFound(*args, **kwargs):
return FakeHttpResponse(404)
name: postInstanceMachineType
category: SampleCrudApi
fakes:
async def Success(*args, **kwargs):
return FakeHttpResponse(200)
async def InternalError(*args, **kwargs):
return FakeHttpResponse(500)
async def NotFound(*args, **kwargs):
return FakeHttpResponse(404)
name: deleteInstance
category: SampleCrudApi
fakes:
async def Success(*args, **kwargs):
return FakeHttpResponse(200)
async def InternalError(*args, **kwargs):
return FakeHttpResponse(500)
async def NotFound(*args, **kwargs):
return FakeHttpResponse(404)
FakerをScenarioに実装する
FakerをScenarioに実装します。
最後に、設定したこれらのfakerをscenarioのcallout部分に実装していきます。
はじめにFakerの実装例を紹介します。
faker("getAccessToken")
r = await callout(
path="/sample_crud_api/get_access_token",
method="GET"
)
このように、scenarioのPython Scriptで外部APIを呼んでいるcallout関数の上にfaker(“faker name”)
の形式で挿入します。
これにより、テスト実行時にこちらのcallout関数は指定したFakerの疑似動作をするようになります。
今回用意したFakerの挿入箇所をまとめておきます。
こちらを参考に、それぞれのscenarioに対してFakerを挿入してください。
faker: getAccessToken
scenario①: InstanceCreate
対象トイブロック: get access token
挿入するcallout関数:
faker("getAccessToken")
r = await callout(
path="/sample_crud_api/get_access_token",
method="GET"
)
scenario②: InstanceUpdate
対象トイブロック: get access token
挿入するcallout関数:
faker("getAccessToken")
r = await callout(
path="/sample_crud_api/get_access_token",
method="GET"
)
scenario③: InstanceDelete
対象トイブロック: get access token
挿入するcallout関数:
faker("getAccessToken")
r = await callout(
path="/sample_crud_api/get_access_token",
method="GET"
)
faker: getInstance
scenario①: InstanceCreate
対象トイブロック: create instance (Cancellation)
挿入するcallout関数:
faker("getInstance")
r = await callout(
path=f"/GoogleCloud/SampleCrudAPI/{new_instance.projectId}/{new_instance.zone}/{new_instance.instanceName}",
method="GET",
headers={
"Authorization": f"Bearer {access_token}"
},
)
scenario②: InstanceDelete
対象トイブロック: check status
挿入するcallout関数:
faker("getInstance")
r = await callout(
path=f"/GoogleCloud/SampleCrudAPI/{delete_instance.projectId}/{delete_instance.zone}/{delete_instance.instanceName}",
method="GET",
headers={
"Authorization": f"Bearer {access_token}"
},
)
faker: getInstanceRunning
scenario①: InstanceCreate
対象トイブロック: check status & update status
挿入するcallout関数:
faker("getInstanceRunning")
r = await callout(
path=f"/GoogleCloud/SampleCrudAPI/{new_instance.projectId}/{new_instance.zone}/{new_instance.instanceName}",
method="GET",
headers={
"Authorization": f"Bearer {access_token}"
},
)
scenario②: InstanceUpdate
対象トイブロック: instance stop (Cancellation)
挿入するcallout関数:
faker("getInstanceRunning")
r = await callout(
path=f"/GoogleCloud/SampleCrudAPI/{update_instance.projectId}/{update_instance.zone}/{update_instance.instanceName}",
method="GET",
headers={
"Authorization": f"Bearer {access_token}"
},
)
scenario③: InstanceUpdate
対象トイブロック: check status & update status
挿入するcallout関数:
faker("getInstanceRunning")
r = await callout(
path=f"/GoogleCloud/SampleCrudAPI/{update_instance.projectId}/{update_instance.zone}/{update_instance.instanceName}",
method="GET",
headers={
"Authorization": f"Bearer {access_token}"
},
)
faker: getInstanceTerminated
scenario①: InstanceUpdate
対象トイブロック: instance stop (Cancellation)
挿入するcallout関数:
faker("getInstanceTerminated")
r = await callout(
path=f"/GoogleCloud/SampleCrudAPI/{update_instance.projectId}/{update_instance.zone}/{update_instance.instanceName}",
method="GET",
headers={
"Authorization": f"Bearer {access_token}"
},
)
scenario②: InstanceUpdate
対象トイブロック: check status & update status
挿入するcallout関数:
faker("getInstanceTerminated")
r = await callout(
path=f"/GoogleCloud/SampleCrudAPI/{update_instance.projectId}/{update_instance.zone}/{update_instance.instanceName}",
method="GET",
headers={
"Authorization": f"Bearer {access_token}"
},
)
scenario③: InstanceUpdate
対象トイブロック: check status & update status (Cancellation)
挿入するcallout関数:
faker("getInstanceTerminated")
r = await callout(
path=f"/GoogleCloud/SampleCrudAPI/{update_instance.projectId}/{update_instance.zone}/{update_instance.instanceName}",
method="GET",
headers={
"Authorization": f"Bearer {access_token}"
},
)
faker: getInstanceMachineType
scenario①: InstanceUpdate
対象トイブロック: post machineType (Cancellation)
挿入するcallout関数:
faker("getInstanceMachineType")
r = await callout(
path=f"/GoogleCloud/SampleCrudAPI/{update_instance.projectId}/{update_instance.zone}/{update_instance.instanceName}",
method="GET",
headers={
"Authorization": f"Bearer {access_token}"
},
)
scenario②: InstanceUpdate
対象トイブロック: check changing machineType
挿入するcallout関数:
faker("getInstanceMachineType")
r = await callout(
path=f"/GoogleCloud/SampleCrudAPI/{update_instance.projectId}/{update_instance.zone}/{update_instance.instanceName}",
method="GET",
headers={
"Authorization": f"Bearer {access_token}"
},
)
faker: postInstance
scenario①: InstanceCreate
対象トイブロック: create instance
挿入するcallout関数:
faker("postInstance")
r = await callout(
path=f"/GoogleCloud/SampleCrudAPI/{new_instance.projectId}/{new_instance.zone}",
method="POST",
headers={
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
},
body={
"machineType": f"zones/{new_instance.zone}/machineTypes/{new_instance.machineType}",
"name": new_instance.instanceName,
"disks": [
{
"initializeParams": {
"sourceImage": "projects/debian-cloud/global/images/family/debian-12"
},
"boot": True
}
],
"networkInterfaces": [
{
"network": "global/networks/default"
}
],
"shieldedInstanceConfig": {
"enableSecureBoot": False
}
}
)
faker: postInstanceStop
scenario①: InstanceUpdate
対象トイブロック: instance stop
挿入するcallout関数:
faker("postInstanceStop")
r = await callout(
path=f"/GoogleCloud/SampleCrudAPI/{update_instance.projectId}/{update_instance.zone}/{update_instance.instanceName}/stop",
method="POST",
headers={
"Authorization": f"Bearer {access_token}"
},
)
faker: postInstanceStart
scenario①: InstanceUpdate
対象トイブロック: instance stop (Cancellation)
挿入するcallout関数:
faker("postInstanceStart")
r = await callout(
path=f"/GoogleCloud/SampleCrudAPI/{update_instance.projectId}/{update_instance.zone}/{update_instance.instanceName}/start",
method="POST",
headers={
"Authorization": f"Bearer {access_token}"
},
)
scenario②: InstanceUpdate
対象トイブロック: instance start
挿入するcallout関数:
faker("postInstanceStart")
r = await callout(
path=f"/GoogleCloud/SampleCrudAPI/{update_instance.projectId}/{update_instance.zone}/{update_instance.instanceName}/start",
method="POST",
headers={
"Authorization": f"Bearer {access_token}"
},
)
faker: postInstanceMachineType
scenario①: InstanceUpdate
対象トイブロック: post machineType
挿入するcallout関数:
faker("postInstanceMachineType")
r = await callout(
path=f"/GoogleCloud/SampleCrudAPI/{update_instance.projectId}/{update_instance.zone}/{update_instance.instanceName}/setMachineType",
method="POST",
headers={
"Authorization": f"Bearer {access_token}"
},
body={
"machineType": f"zones/{update_instance.zone}/machineTypes/{machine_type}"
}
)
faker: deleteInstance
scenario①: InstanceDelete
対象トイブロック: delete instance
挿入するcallout関数:
faker("deleteInstance")
r = await callout(
path=f"/GoogleCloud/SampleCrudAPI/{delete_instance.projectId}/{delete_instance.zone}/{delete_instance.instanceName}",
method="DELETE",
headers={
"Authorization": f"Bearer {access_token}"
},
)
以上でFakerの実装が完了です。
Illusionの実装
今回作成するIllusion
今回のSample CRUD APIでは、以下のIllusionを用意します。
Illusion名 | 想定する場合 |
---|---|
CreateSuccess | CREATE APIが成功する場合 |
CreateInstanceFailed | CREATE APIでインスタンスの作成に失敗する場合 |
UpdateSuccess | UPDATE APIが成功する場合 |
UpdateMachineTypeFailed | UPDATE APIでマシンタイプの変更に失敗する場合 |
DeleteSuccess | DELETE APIが成功する場合 |
DeleteInstanceFailed | DELETE APIでインスタンスの削除に失敗する場合 |
Note
IllusionとはFakerがどの挙動をするかをまとめたものになります。
例えば、上記のCreateSuccess
はCREATE APIが成功する場合のIllusionになりますが、この場合のFakerは以下をセットします。
getAccessToken
:Success
postInstance
:Success
getInstanceRunning
:Running
CREATE APIが成功する場合、CREATE APIが呼び出すGoogle Cloud APIはすべて成功することになるため、そのようにふるまうようにFakerを指定しています。
一方で上記のCreateInstanceFailed
ではCREATE APIのインスタンスの作成に失敗する場合を再現するため、以下のようにFakerをセットします。
getAccessToken
:Success
postInstance
:InternalError
getInstanceRunning
:NotFound
getInstance
:NotFound
これはインスタンス作成(postInstance
)がInternalError
になる場合を再現しています。
インスタンス作成に失敗すると、作成後にインスタンスをGETしてもNotFound
になることからそれもFakerで表現しています。
このように、テストケースを作成する際に、いちいちFakerを設定するのではなく、IllusionでFakerをまとめて設定をすることができるようになります。
Illusionの新規作成
それでは一つずつillusionを作成していきます。
[Unit Test as a Service]>[Illusion]>[Create New Illusion]
からIllusionを作成することができます。
category
、name
を入力してillusionを作成します。
作成したIllusionを開きます。
Fakers to use
タブを開き、Fakers
タブのうち、Illusionに含めたいFakerをドラッグアンドドロップでFakers to use
に移動させます。
その際に、Fakerにどのふるまいをさせるかを選択します。
最終的に以下のようになります。
上記の例では、IllusionのCreateSuccess
のFakerは
- getAccessToken:Successの挙動をする
- postInstance:Successの挙動をする
- getInstanceRunning:Runningの挙動をする
となります。
上記を参考に、今回のSample CRUD APIでは以下のIllusionを作成します。
name | category | fakes |
---|---|---|
CreateSuccess | SampleCrudApi | getAccessToken - Success postInstance - Success getInstanceRunning - Running |
CreateInstanceFailed | SampleCrudApi | getAccessToken - Success postInstance - InternalError getInstanceRunning - NotFound getInstance - NotFound |
UpdateSuccess | SampleCrudApi | getAccessToken - Success postInstanceStop - Success getInstanceTerminated - Terminated postInstanceMachineType - Success getInstanceMachineType - e2small postInstanceStart - Success getInstanceRunning - Running |
UpdateMachineTypeFailed | SampleCrudApi | getAccessToken - Success postInstanceStop - Success getInstanceTerminated - Terminated postInstanceMachineType - InternalError getInstanceMachineType - e2micro postInstanceStart - Success getInstanceRunning - Running |
DeleteSuccess | SampleCrudApi | getAccessToken - Success deleteInstance - Success getInstance - NotFound |
DeleteInstanceFailed | SampleCrudApi | getAccessToken - Success deleteInstance - InternalError |
以上でIllusionの実装が完了です。
Testcaseの実装
今回作成するTestcase
今回のSample CRUD APIでは、以下のTestcaseを用意します。
Test Case | 適用するIllusion | 想定するケース |
---|---|---|
CreateSuccess | CreateSuccess | CREATE APIが成功するケース |
CreateInstanceFailed | CreateInstanceFailed | CREATE APIでインスタンスの作成に失敗するケース |
GetSuccess | GetSuccess | READ APIが成功するケース |
UpdateSuccess | UpdateSuccess | UPDATE APIが成功するケース |
UpdateMachineTypeFailed | UpdateMachineTypeFailed | UPDATE APIでマシンタイプの変更に失敗するケース |
DeleteSuccess | DeleteSuccess | DELETE APIが成功するケース |
DeleteInstanceFailed | DeleteInstanceFailed | DELETE APIでインスタンスの削除に失敗するケース |
Note
今回はチュートリアルということで、各CRUD APIの成功ケースと、CREATE、UPDATE、DELETEにおける代表的な失敗ケースの計7ケースを用意しています。
しかし、実際の運用を考えると失敗ケースはほかにも考えられますので、自分の実装するアプリケーションをテストする場合は、十分にケースを考えるようにしましょう。
Testcaseの新規作成
それでは一つずつTestcaseを作成していきます。
例として、今回作成するTestcaseの一つである、CreateSuccess
を作成してみます。
[Unit Test as a Service]>[Test Case]>[Create New TestCase]
からTestcaseを作成することができます。
まず、何に対するテストケースかを選択します。今回はScenarioに対するテストのため、Scenarioを選択してください。
次にTestcaseのname
、category
、target
を入力します。
target
には、今回テストしたいScenarioを選択してください。
今回は以下を入力してください。
- name : CreateSuccess
- category : SampleCrudApi
- target : InstanceCreate
Testcaseの設定
作成したTestcaseを選択し、Testcase Setting
タブを開きます。
先ほど設定したname
、category
、target
に加えて、illusionをここで設定することができます。
今回はillusionに以下を設定してください。
- illusion : CreateSuccess
Perspective Setting
タブにはFakerの挙動が表示されます。今回は上で指定したillusionの内容が入っていることが確認できます。
Flow Settingの設定
続いて、具体的にこのTestcaseの中身を実装していきます。
Flow Setting
タブを開いてください。
Note
それぞれの項目の説明は以下になります。 特にAPIの呼び出しはScenario Inputで行います。
-
Preparation(準備):
テスト実行前の初期化や環境設定を行います。たとえば、テスト対象のデータやモックのセットアップを行います。 -
Assert Begin(検証開始):
テストの開始を宣言し、後続の検証処理が始まることを示します。 -
Scenario Input(シナリオ入力):
実際にテスト対象のAPI呼び出しや操作などの入力処理を記述します。 -
Scenario Output(シナリオ出力):
テスト実行後の応答結果や状態変化を確認・記録します。出力内容が期待通りかをチェックするための処理です。 -
Assert End(検証終了):
テストシナリオの終了を明示し、全体の結果がまとめられます。検証ロジックの完了を示します。 -
Cleanup(後処理):
テスト実行後に環境を元に戻すための処理です。たとえば、作成したテストデータの削除やリソースの解放などを行います。
今回のTestcaseでは以下のように実装します。
Testcase①
name: CreateSuccess
target: InstanceCreate(CREATE API)
illusion: CreateSuccess
Preparation:
async def hoge(*args, **kwargs):
pass
Assert Begin:
async def hoge(*args, **kwargs):
pass
Scenario Input:
- method: POST
- path:
/sample_crud_api/instances
- headers:
{
"Content-Type": "application/json"
}
- body:
{
"instance_name": "test-instance"
}
Note
APIの呼び出しはScenario Inputで行います。
このテストケースではCREATE APIのテストを行うため、Scenario Input
では、CREATE APIの呼び出しを行います。
Scenario Input (script):
- get access token:-
- insert atom: -
- create instance: -
- check status & update status: -
Scenario Output:
async def assertion():
assert Response.code==202, "Invalid response code %r" % Response.code
Note
Scenario OutputではAPIを呼び出したあとのレスポンスを検証できます。
CERATE APIは202が返却されることが期待されるため、202が返却されたかどうかを確認します。
Scenario Output (script):
- get access token:-
- insert atom: -
- create instance: -
- check status & update status: -
Assert End:
async def assertion():
assert Transaction.status=="Complete", "Transaction is in an unexpected state %r" % Transaction.status
Note
Assert Endでは最終的な結果を検証できます。
このテストケースはCREATE APIが正常に完了した場合のテストになります。そのため、最終的にトランザクションのステータスがCompleteであるかどうかを確認します。
Cleanup:
async def cleanup():
# テスト用のInstanceを削除する
test_instances = await atom.VmInstance.retrieve(instanceName="test-instance")
test_instance = test_instances[0]
await test_instance.destroy()
# テスト用のInstanceの削除を確認する
test_instances = await atom.VmInstance.retrieve(instanceName="test-instance")
assert test_instances==[], "Test instance exists"
Note
Cleanupでは環境のクリーンアップを行います。このテストを実行したことで、CREATE APIが呼び出され、データベースにtest-instance
のレコードが追加されました。
そのため、作成されたtest-instance
のレコードをクリーンアップで削除し、テスト実行前の状態に戻しています。
Caution
Cleanupが正しく行われたかの判断は、本Testcaseで作成したtest-instance
という名前のVmInstance
が削除されたかどうかで判断します。
チュートリアルを実施する中でtest-instance
という名前のインスタンスを複数作成している場合、Cleanupでは正しく削除されず、Cleanup後にtest-instance
が残ってしまいます。
そのため、Testを実行する際には事前にtest-instance
という名前のVmInstance
は削除しましょう。
上記を参考に、そのほかのTestcaseについても実装してください。
Testcase②
name: CreateInstanceFailed
target: InstanceCreate(CREATE API)
illusion: CreateInstanceFailed
Preparation:
async def hoge(*args, **kwargs):
pass
Assert Begin:
async def hoge(*args, **kwargs):
pass
Scenario Input:
- method: POST
- path:
/sample_crud_api/instances
- headers:
{
"Content-Type": "application/json"
}
- body:
{
"instance_name": "test-instance"
}
Scenario Input (script):
- get access token:-
- insert atom: -
- create instance: -
- check status & update status: -
Scenario Output:
async def assertion():
assert Response.code==202, "Invalid response code %r" % Response.code
Scenario Output (script):
- get access token:-
- insert atom: -
- create instance: -
- check status & update status: -
Assert End:
async def assertion():
assert Transaction.status=="Cancelled", "Transaction is in an unexpected state %r" % Transaction.status
Note
このテストケースはCREATE APIが途中で失敗し、ロールバックとなる場合のテストになります。そのため、最終的にトランザクションのステータスがロールバックが正常に完了した場合のステータスであるCancelledであるかどうかを確認します。
Cleanup:
async def cleanup():
# テスト用のInstanceの削除を確認する
test_instances = await atom.VmInstance.retrieve(instanceName="test-instance")
assert test_instances==[], "Test instance exists"
Testcase③
name: GetSuccess
target: InstanceGet(READ API)
illusion: -
Preparation:
async def preparation():
#テスト用のVmInstanceクラスのインスタンスを作成する
test_instance = await atom.VmInstance(
instanceName="test-instance",
status="RUNNING",
machineType="e2-micro"
)
# データベースに保存する
await test_instance.save()
Note
Preparationはテスト開始前にテストデータのセットアップができます。
このテストはREAD APIに対するテストのため、GET対象のデータを事前に用意しておきます。
Assert Begin:
async def hoge(*args, **kwargs):
pass
Scenario Input:
- method: GET
- path:
/sample_crud_api/instances/test-instance
Scenario Input (script):
- get instances from atom:-
Scenario Output:
async def assertion():
assert Response.code==200, "Invalid response code %r" % Response.code
Scenario Output (script):
- get instances from atom: -
Assert End:
async def hoge(*args, **kwargs):
pass
Cleanup:
async def cleanup():
# テスト用のInstanceを削除する
test_instances = await atom.VmInstance.retrieve(instanceName="test-instance")
test_instance = test_instances[0]
await test_instance.destroy()
# テスト用のInstanceの削除を確認する
test_instances = await atom.VmInstance.retrieve(instanceName="test-instance")
assert test_instances==[], "Test instance exists"
Testcase④
name: UpdateSuccess
target: InstanceUpdate(UPDATE API)
illusion: UpdateSuccess
Preparation:
async def preparation():
#テスト用のVmInstanceクラスのインスタンスを作成する
test_instance = await atom.VmInstance(
instanceName="test-instance",
status="RUNNING",
machineType="e2-micro"
)
# データベースに保存する
await test_instance.save()
Note
このテストはUPDATE APIに対するテストのため、UPDATE対象のデータを事前に用意しておきます。
Assert Begin:
async def hoge(*args, **kwargs):
pass
Scenario Input:
- method: PUT
- path:
/sample_crud_api/instances/test-instance
- headers:
{
"Content-Type": "application/json"
}
- body:
{
"machine_type": "e2-small"
}
Scenario Input (script):
- get access token:-
- target machineType: -
- instance stop: -
- check status & update status: -
- post machineType: -
- check changing machineType: -
- instance start: -
- check status & update status: -
- update machineType: -
Scenario Output:
async def assertion():
assert Response.code==202, "Invalid response code %r" % Response.code
Scenario Output (script):
- get access token:-
- target machineType: -
- instance stop: -
- check status & update status: -
- post machineType: -
- check changing machineType: -
- instance start: -
- check status & update status: -
- update machineType: -
Assert End:
async def assertion():
assert Transaction.status=="Complete", "Transaction is in an unexpected state %r" % Transaction.status
Cleanup:
async def cleanup():
# テスト用のInstanceを削除する
test_instances = await atom.VmInstance.retrieve(instanceName="test-instance")
test_instance = test_instances[0]
await test_instance.destroy()
# テスト用のInstanceの削除を確認する
test_instances = await atom.VmInstance.retrieve(instanceName="test-instance")
assert test_instances==[], "Test instance exists"
Testcase⑤
name: UpdateMachineTypeFailed
target: InstanceUpdate(UPDATE API)
illusion: UpdateMachineTypeFailed
Preparation:
async def preparation():
#テスト用のVmInstanceクラスのインスタンスを作成する
test_instance = await atom.VmInstance(
instanceName="test-instance",
status="RUNNING",
machineType="e2-micro"
)
# データベースに保存する
await test_instance.save()
Assert Begin:
async def hoge(*args, **kwargs):
pass
Scenario Input:
- method: PUT
- path:
/sample_crud_api/instances/test-instance
- headers:
{
"Content-Type": "application/json"
}
- body:
{
"machine_type": "e2-small"
}
Scenario Input (script):
- get access token:-
- target machineType: -
- instance stop: -
- check status & update status: -
- post machineType: -
- check changing machineType: -
- instance start: -
- check status & update status: -
- update machineType: -
Scenario Output:
async def assertion():
assert Response.code==202, "Invalid response code %r" % Response.code
Scenario Output (script):
- get access token:-
- target machineType: -
- instance stop: -
- check status & update status: -
- post machineType: -
- check changing machineType: -
- instance start: -
- check status & update status: -
- update machineType: -
Assert End:
async def assertion():
assert Transaction.status=="Cancelled", "Transaction is in an unexpected state %r" % Transaction.status
test_instances = await atom.VmInstance.retrieve(instanceName="test-instance")
test_instance = test_instances[0]
assert test_instance.machineType=="e2-micro", "MachineType is unexpected"
assert test_instance.status=="RUNNING", "Status is unexpected"
Note
このテストケースはUPDATE APIが途中で失敗し、ロールバックとなる場合のテストになります。そのため、最終的にトランザクションのステータスがロールバックが正常に完了した場合のステータスであるCancelledであるかどうかを確認します。
また、SDK側のインスタンスのマシンタイプとステータスが、テスト実行前のe2-microとRUNNINGに戻っているかも確認しています。これにより、キャンセル処理が正常に行われたかどうかを確認しています。
Cleanup:
async def cleanup():
# テスト用のInstanceを削除する
test_instances = await atom.VmInstance.retrieve(instanceName="test-instance")
test_instance = test_instances[0]
await test_instance.destroy()
# テスト用のInstanceの削除を確認する
test_instances = await atom.VmInstance.retrieve(instanceName="test-instance")
assert test_instances==[], "Test instance exists"
Testcase⑥
name: DeleteSuccess
target: InstanceDelete(DELETE API)
illusion: DeleteSuccess
Preparation:
async def preparation():
#テスト用のVmInstanceクラスのインスタンスを作成する
test_instance = await atom.VmInstance(
instanceName="test-instance",
status="RUNNING",
machineType="e2-micro"
)
# データベースに保存する
await test_instance.save()
Assert Begin:
async def hoge(*args, **kwargs):
pass
Scenario Input:
- method: DELETE
- path:
/sample_crud_api/instances/test-instance
Scenario Input (script):
- get access token:-
- insert atom: -
- create instance: -
- check status & update status: -
Scenario Output:
async def assertion():
assert Response.code==202, "Invalid response code %r" % Response.code
Scenario Output (script):
- get access token:-
- insert atom: -
- create instance: -
- check status & update status: -
Assert End:
async def assertion():
assert Transaction.status=="Complete", "Transaction is in an unexpected state %r" % Transaction.status
Cleanup:
async def cleanup():
# テスト用のInstanceを削除する
test_instances = await atom.VmInstance.retrieve(instanceName="test-instance")
test_instance = test_instances[0]
await test_instance.destroy()
# テスト用のInstanceの削除を確認する
test_instances = await atom.VmInstance.retrieve(instanceName="test-instance")
assert test_instances==[], "Test instance exists"
Testcase⑦
name: DeleteInstanceFailed
target: InstanceDelete(DELETE API)
illusion: DeleteInstanceFailed
Preparation:
async def preparation():
#テスト用のVmInstanceクラスのインスタンスを作成する
test_instance = await atom.VmInstance(
instanceName="test-instance",
status="RUNNING",
machineType="e2-micro"
)
# データベースに保存する
await test_instance.save()
Assert Begin:
async def hoge(*args, **kwargs):
pass
Scenario Input:
- method: DELETE
- path:
/sample_crud_api/instances/test-instance
Scenario Input (script):
- get access token:-
- insert atom: -
- create instance: -
- check status & update status: -
Scenario Output:
async def assertion():
assert Response.code==202, "Invalid response code %r" % Response.code
Scenario Output (script):
- get access token:-
- insert atom: -
- create instance: -
- check status & update status: -
Assert End:
async def assertion():
assert Transaction.status=="Aborted", "Transaction is in an unexpected state %r" % Transaction.status
Note
このテストケースはDELETE APIが途中で失敗した場合のテストになります。
DELETE APIはロールバックを許可していないため、トランザクションのステータスはAbortedのままになるはずです。
そのため、最終的にトランザクションのステータスがAbortedであるかどうかを確認します。
Cleanup:
async def cleanup():
# テスト用のInstanceを削除する
test_instances = await atom.VmInstance.retrieve(instanceName="test-instance")
test_instance = test_instances[0]
await test_instance.destroy()
# テスト用のInstanceの削除を確認する
test_instances = await atom.VmInstance.retrieve(instanceName="test-instance")
assert test_instances==[], "Test instance exists"
以上でTest Caseの実装が完了です。
Testsuiteの実装
今回のSample CRUD APIでは、以下のTestsuiteを用意します。
Testsuite | 説明 |
---|---|
SampleCrudApiSuite | Sample CRUD APIの全テストケースを集めたもの |
Note
Testsuiteとはいくつかのテストケースをまとめたものになります。
ユニットテストでは、テストケースを一つずつ実行することもできますが、テストスイートを実行することもできます。
そのため、事前に複数のテストケースをテストスイートにまとめておくことでユニットテストの実行が簡単になります。
今回のSample CRUD APIでは、すべてのテストケースを一つのテストスイートにまとめています。
Testsuiteの新規作成
それではTest Suiteを作成していきます。
[Unit Test as a Service]>[Testsuites]>[Create New Testsuite]
からTestsuiteを作成することができます。
category
、name
を入力してTestsuiteを作成します。
今回は以下を入力します。
name: SampleCrudApiSuite
category: SampleCrudApi
作成したTestsuiteを開きます。
Testcase to execute
タブを開き、右のTestcases
タブのうち、Testsuiteに含めたいTestcasesをドラッグアンドドロップでTestcase to execute
に移動させます。
今回はすべてのテストケースをSampleCrudApiSuiteに移動させます。
以上でTestsuiteの実装が完了です。
Unit Testの実行
最後に作成したテストを実行してみます。
Caution
Testの実行前にtest-instance
という名前のVmInstance
は削除しておきましょう。
削除しないとTestcaseで正しくCleanupされません。
まずUnit Test
を開きましょう。
Test Scenarioに先ほど作成したTestcase
とTestsuite
があると思います。
SampleCrudApiSuiteを選択し、実行ボタンを押下して実行してみましょう。
テストが順に実行されて以下のような結果が表示されると思います。
100%となればすべてのテストにPassしたことになります。
ーー
以上、Sample CRUD APIのチュートリアルになります。