aws cliには API 呼び出し後、特定のステータスになるまでポーリングする wait
コマンドがあります。
例えば ec2 インスタンスを起動する API を呼び出し後、インスタンスの起動が完了するまで待つには $ aws ec2 wait instance-running --instance-ids xxx
のようにします。
この wait 機能が実装されるまでは
#!/bin/bash
instance_id=$(aws ec2 run-instances –image-id ami-12345 \
--query Reservations[].Instances[].InstanceId \
--output text)
instance_state=$(aws ec2 describe-instances –instance-ids $instance_id
--query 'Reservations[].Instances[].State.Name')
while [ "$instance_state" != "running" ]
do
sleep 1
instance_state=$(aws ec2 describe-instances –instance-ids $instance_id \ --query 'Reservations[].Instances[].State.Name')
done
というように、ポーリング処理を自前で実装しなければいけ ませんでした(例外処理も真面目にやるとさらにごちゃごちゃする)。
この wait 系コマンドを独自追加する方法をメモ。
cloudformationでスタック構築完了を待つ
例として cloudformation でスタックの作成APIをたたいたあと
(crete-stack
)、スタックの構築が完了するまでポーリングするコマンド $ aws cloudformation wait stack-completed
を実装してみましょう。
AWS CLI で wait を使わず cloudformation のスタックを構築
wait を使わずに cloudformation のスタックを構築するには
$ aws cloudformation create-stack --stack [STACKNAME]
でスタックの作製命令をし
$ aws cloudformation describe-stacks --stack-name [STACKNAME]
で StackStatus が CREATE_COMPLETE
になるかチェックすることになるかと思います。
$ aws cloudformation create-stack --stack-name SampleStack --template-body file://SNSToSQS.template \
--parameters ParameterKey=MyPublishUserPassword,ParameterValue=password \
ParameterKey=MyQueueUserPassword,ParameterValue=password \
--capabilities CAPABILITY_IAM
{
"StackId": "arn:aws:cloudformation:us-east-1:01234:stack/SampleStack/30811fd0-1215-11e5-aacf-50018ffe9e62"
}
$ aws cloudformation describe-stacks --stack-name SampleStack
{
"Stacks": [
{
"StackId": "arn:aws:cloudformation:ap-northeast-1:01234:stack/SampleStack/db83f660-3753-11e5-a35a-50fa594fb836",
"Description": "...",
"Parameters": [
{
"ParameterValue": "****",
"ParameterKey": "MyPublishUserPassword"
},
{
"ParameterValue": "****",
"ParameterKey": "MyQueueUserPassword"
}
],
"Tags": [],
"CreationTime": "2015-08-01T07:15:00.948Z",
"Capabilities": [
"CAPABILITY_IAM"
],
"StackName": "SampleStack",
"NotificationARNs": [],
"StackStatus": "CREATE_IN_PROGRESS",
"DisableRollback": false
}
]
}
$ aws cloudformation describe-stacks --stack-name SampleStack
{
"Stacks": [
{
...,
"StackName": "SampleStack",
"StackStatus": "CREATE_COMPLETE",
...,
}
]
}
AWS CLI wait に落としこむ
では、これを AWS CLI の wait コマンドに落としこんでみましょう。
ポイント AWS CLI はデータドリブンであるということです。
API のリクエスト/レスポンスは JSON ファイル(データ)でモデル定義します。
wait コマンドも例外ではなく
- wait のコマンド名
- ポーリング時にステータスチェックするコマンド
- ポーリング間隔
などを JSON で定義します。
天下り的になりますが、cloudformation の stack-completed wait 用の JSON モデルは次のようになります。
{
"version": 2,
"waiters": {
"StackCompleted": {
"operation": "DescribeStacks",
"delay": 30,
"maxAttempts": 30,
"acceptors": [
{
"expected": 200,
"argument": "Stacks[].StackStatus",
"expected": "CREATE_COMPLETE",
"state": "success",
"matcher": "pathAll"
}
]
}
}
}
重要な箇所をかいつまんで説明します。
StackCompleted
が wait のコマンド名。
AWS Web API のメソッド名は CamelCase なので、モデルでもそのようにします。
AWS CLI から使うときは FooBar は foo_bar のように変換されるので、 StackCompleted
は stack_completed
となります。
"operation": "DescribeStacks"
はポーリング時に問い合わせる API です。
"delay": 30
はポーリング間隔です。単位は秒です。
"maxAttempts": 30
はポーリングのリトライ数です。
この数を超えても適切な状態に遷移しなかった場合、ステータスコード 255 で AWS CLI のプロセスが終了します。
"acceptors"
のブロックは "operation"(DescribeStacks)
のレスポンスに対する処理を記述します。
今回のケースでは StackStatus が CREATE_COMPLETE
に遷移した時に wait 完了と判断します。
より具体的には API DescribeStacks に対するレスポンスボディー
{
"Stacks": [
{
...,
"StackName": "SampleStack",
"StackStatus": "CREATE_COMPLETE",
}
]
}
に対して JMESPATH で Stacks[].StackStatus
というようにステータス を抽出し、期待値 CREATE_COMPLETE
と一致するか判定します。
JMESPATH の expression
を argument
で、期待値を expected
で記述します。
"state": "success"
により、条件が満たされた時は正常終了します。
"expected": 200
は HTTP レスポンスステータスです。
この JSON ファイルを botocore の cloudformation 用モデル定義ディレクトリ botocore/data/cloudformation/2010-05-15
に waiters-2.json
という名前で新規作成します。
(ec2 などと異なり cloudformation には wait が未定義なので新規ファイルとして作成します)
結果的に、以下の様なファイル構成になります。
botocore/data/cloudformation/
└── 2010-05-15
├── paginators-1.json
├── service-2.json
└── waiters-2.json # <- NEW!
botocore のディレクトリは SHELL から以下のコマンドで確認できます。
$ python -c 'import botocore;print (botocore.__file__)'
/Users/jsmith/venv/lib/python2.7/site-packages/botocore/__init__.pyc
オレオレ wait を使ってみる
それでは追加した wait コマンドを実際に使ってみましょう。
ヘルプメッセージの表示
まずはヘルプコマンドを確認
$ aws cloudformation wait help
WAIT() WAIT()
NAME
wait -
DESCRIPTION
Wait until a particular condition is satisfied.
AVAILABLE COMMANDS
o stack-completed
WAIT()
$ aws cloudformation wait stack-completed help
STACK-COMPLETED() STACK-COMPLETED()
NAME
stack-completed -
DESCRIPTION
Wait until JMESPath query Stacks[].StackStatus returns CREATE_COMPLETE
for all elements when polling with describe-stacks. It will poll every
30 seconds until a successful state has been reached. This will exit
with a return code of 255 after 30 failed checks.
SYNOPSIS
stack-completed
[--stack-name <value>]
[--cli-input-json <value>]
[--starting-token <value>]
[--max-items <value>]
[--generate-cli-skeleton]
OPTIONS
--stack-name (string)
The name or the unique stack ID that is associated with the stack,
which are not always interchangeable:
...
ヘルプは問題なさそうです。
stack-completed
は内部的には action
で定義したように describe-stacks
を呼び出しているだけなので describe-stacks
の引数(--stack-name
)がそのまま使えます。
スタックの作製
次の実際にスタックを作成して、wait コマンドで構築完了を待ちます。
SNS/SQS を構築する次のテンプレートを利用します。
$ aws cloudformation create-stack --stack-name SampleStack --template-body file://SNSToSQS.template \
--parameters ParameterKey=MyPublishUserPassword,ParameterValue=password \
ParameterKey=MyQueueUserPassword,ParameterValue=password \
--capabilities CAPABILITY_IAM
$ aws cloudformation wait stack-completed --stack-name SampleStack --debug
wait
実行時に --debug
オプションをつけると delay
で指定した間隔で DescribeStacks
を実行しているのがよく分かるかと思います。
エラー処理の強化
--stack-name
に存在しないスタック名を指定すると、次のようにエラーが発生します。
$ aws cloudformation wait stack-completed --stack-name NXStackName
Waiter StackCompleted failed: Unexpected error encountered.
--debug
オプションをつけてレスポンスを確認すると、
<ErrorResponse xmlns="http://cloudformation.amazonaws.com/doc/2010-05-15/">
<Error>
<Type>Sender</Type>
<Code>ValidationError</Code>
<Message>Stack with id SampleStack2 does not exist</Message>
</Error>
<RequestId>12345</RequestId>
</ErrorResponse>
というようなレスポンスが返ってきており、先ほど作成したモデルではこのようなレスポンスを考慮していません。
JSON ファイルの acceptors
に ValidationError
用のモデルを追加します。
{
"version": 2,
"waiters": {
"StackCompleted": {
"delay": 30,
"operation": "DescribeStacks",
"maxAttempts": 30,
"acceptors": [
{
"expected": 200,
"argument": "Stacks[].StackStatus",
"state": "success",
"expected": "CREATE_COMPLETE",
"matcher": "pathAll"
},
{
"matcher": "error",
"expected": "ValidationError",
"state": "success"
}
]
}
}
}
"matcher": "error"
でレスポンスの Error
ブロックを抽出します。
"expected" : "ValidationError"
でエラーコードを突き合わせます。
このように JSON を書き換えた上で再度存在しないスタック名を wait コマンドに食わせると、今度は正常に処理されました。
$ aws cloudformation wait stack-completed --stack-name NXStackName
$ echo $?
0
Stack のステータスはロールバック系など様々なステータスが存在するので、本運用で使うなら例外系処理を中心にもっとまじめに条件を書かないといけません。
boto3 から使ってみる
今回ハック下 botocore の JSONモデルは
でも利用されています。
boto3 は JSON モデルだけでなく botocore パッケージそのものが共有されているので、カスタマイズした botocore を boto3 からも呼び出してみましょう。
import boto3
import botocore
StackName = 'TEST'
Template = 'https://s3.amazonaws.com/cloudformation-templates-us-east-1/SNSToSQS.template'
client = boto3.client('cloudformation')
client.create_stack(
StackName=StackName,
TemplateURL=Template,
Parameters=[
{
'ParameterKey': 'MyPublishUserPassword',
'ParameterValue': 'password'
},
{
'ParameterKey': 'MyQueueUserPassword',
'ParameterValue': 'password'
}
],
Capabilities=['CAPABILITY_IAM'],
)
waiter = client.get_waiter('stack_completed') # your own waiter
waiter.wait(StackName=StackName)
print 'do something'
まとめ
AWS CLI の wait 系コマンドはそれほど熱心にはコマンド追加されていないので、業務で必要なものは botocore をいじる(fork)ことで独自に拡張できることを理解いただけたかと思います。
References