AWS CLIにオレオレwaitコマンドを追加する

aws
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 のように変換されるので、 StackCompletedstack_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",
        }
    ]
}

に対して JMESPATHStacks[].StackStatus というようにステータス を抽出し、期待値 CREATE_COMPLETE と一致するか判定します。
JMESPATH の expressionargument で、期待値を expected で記述します。

"state": "success" により、条件が満たされた時は正常終了します。

"expected": 200 は HTTP レスポンスステータスです。

この JSON ファイルを botocore の cloudformation 用モデル定義ディレクトリ botocore/data/cloudformation/2010-05-15waiters-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 ファイルの acceptorsValidationError 用のモデルを追加します。

{
  "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

Leave a comment