CloudFormation 一撃で停止できるEC2スポットインスタンスを立ててみた with カスタムリソース
AWS事業本部梶原@福岡オフィスです。
ちょっと手軽にスポットインスタンスでAmazonLinux2の検証がしたいっていうときの起動テンプレートです。
普通に停止できるEC2スポットインスタンスを立てようとして永続リクエストを有効にして、CloudFormationでEC2インスタンスをたてるとその際に永続的なスポットリクエストが生成されます。
こちらはCloudFormationの管理外となり、
CloudFormationのスタックを削除しても残ります。残ります。残ります。
残った永続的なスポットリクエストにより、一定時間たつと新しいEC2インスタンスが立ち上がります。
まじか(滝汗
ということで、CloudFormationのスタックを消した際には一緒に永続的なスポットリクエストも消したくなりました
イベント検知とかいろいろ試行錯誤したんですが、結局、伝家の宝刀諸刃の剣のカスタムリソースでバッサリ消しました。
結構危うい処理なので、あえてカスタムリソースとは?とか、スポットインスタンスとは?とかは、割愛させていただきます。 リスクをご承知の上で存分にご活用いただければと思います。
クイック作成リンク
AWSコンソールにログイン後
すると下記記載のEC2がスポットインスタンスで起動します。 停止することも可能です。不要になった際はCloudFormationのスタック毎消してください。カスタムリソースでスポットリクエストを消してますが、動作保証しません。一応消えているか確認してください。
使用しているテンプレートの全体はページ末尾に記載しています。
EC2スポットインスタンス
項目 | 値 | 備考 |
---|---|---|
AMI | Amazon Linux 2 | SSMパラメータより最新を取得 |
InstanceType | t3.micro | パラメータにしているので指定してください |
スポット価格最高値 | 指定なし | 未指定ですのでオンデマンド価格で入札です |
ネットーワークとか | 指定なし | 標準のVPCで起動します |
接続方法 | SSM | 必要なRole(AmazonSSMManagedInstanceCore)はテンプレートで作成しています |
スポットリクエストキャンセルカスタムリソース
項目 | 値 | 備考 |
---|---|---|
Runtime | python3.6 | |
MemorySize | 128MB | |
Timeout | 120 | |
Role | AWSLambdaBasicExecutionRole, AmazonEC2FullAccess |
Lambdaで以下の動作をさせています。
パラメータで渡されたインスタンスIDよりスポットリクエストIDを取得します。 各処理正常終了時はスポットリクエストIDを返します
新規作成時
スポットリクエストIDを取得しているだけです。
response = client.describe_instances(**params) SpotInstanceRequestId = response['Reservations'][0]['Instances'][0]['SpotInstanceRequestId']
更新時
更新前インスタンスのスポットリクエストのキャンセルを行います。
更新後のスポットリクエストIDを取得します
old_params = dict([(k, v) for k, v in event['OldResourceProperties'].items() if k != 'ServiceToken']) response = client.describe_instances(**old_params) SpotInstanceRequestId = response['Reservations'][0]['Instances'][0]['SpotInstanceRequestId'] response = client.cancel_spot_instance_requests( SpotInstanceRequestIds=[ SpotInstanceRequestId, ], ) response = client.describe_instances(**params) SpotInstanceRequestId = response['Reservations'][0]['Instances'][0]['SpotInstanceRequestId']
削除時
スポットリクエストのキャンセルを行います。
response = client.describe_instances(**params) SpotInstanceRequestId = response['Reservations'][0]['Instances'][0]['SpotInstanceRequestId'] response = client.cancel_spot_instance_requests( SpotInstanceRequestIds=[ SpotInstanceRequestId, ], )
まとめ
手軽にスポットインスタンスを使おうとしていたのですが、ちょっと大掛かりになってしまいました。 とはいえ、これでポチっとすれば、安心して、スポットインスタンスで最大%OFFな感じで検証できて スタック消せばきれいに消えるのでお手軽です。
参考URL
Boto3 Cancels one or more Spot Instance requests.
テンプレート
https://pub-devio-blog-qrgebosd.s3-ap-northeast-1.amazonaws.com/template/cfn-ec2-spot-instance.yml
Parameters: AMIId: Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id> Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2 InstanceType: Type: String Default: t3.micro Resources: EC2Instance: Type: AWS::EC2::Instance Properties: ImageId: !Ref AMIId IamInstanceProfile: !Ref AmazonSSMManagedInstanceProfile LaunchTemplate: LaunchTemplateId: !Ref ECSplotLaunchTemplate Version: !GetAtt ECSplotLaunchTemplate.LatestVersionNumber ECSplotLaunchTemplate: Type: AWS::EC2::LaunchTemplate Properties: LaunchTemplateData: InstanceType: !Ref InstanceType InstanceMarketOptions: MarketType: spot SpotOptions: InstanceInterruptionBehavior: stop MaxPrice: !Ref 'AWS::NoValue' # OnDemand SpotInstanceType: persistent AmazonSSMManagedInstanceCoreRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: ec2.amazonaws.com Action: sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore AmazonSSMManagedInstanceProfile: Type: AWS::IAM::InstanceProfile Properties: Roles: - !Ref AmazonSSMManagedInstanceCoreRole CancelSpotInstanceRequestsLambda: Type: AWS::Lambda::Function Properties: Code: ZipFile: | import json import boto3 import cfnresponse def handler(event, context): try: print(event) params = dict([(k, v) for k, v in event['ResourceProperties'].items() if k != 'ServiceToken']) client = boto3.client('ec2') if event['RequestType'] == 'Create': response = client.describe_instances(**params) print(response) SpotInstanceRequestId = response['Reservations'][0]['Instances'][0]['SpotInstanceRequestId'] responseData = {} responseData['SpotInstanceRequestId'] = SpotInstanceRequestId cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, SpotInstanceRequestId) if event['RequestType'] == 'Delete': response = client.describe_instances(**params) print(response) SpotInstanceRequestId = response['Reservations'][0]['Instances'][0]['SpotInstanceRequestId'] response = client.cancel_spot_instance_requests( SpotInstanceRequestIds=[ SpotInstanceRequestId, ], ) print(response) responseData = {} responseData['SpotInstanceRequestId'] = SpotInstanceRequestId cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, SpotInstanceRequestId) if event['RequestType'] == 'Update': old_params = dict([(k, v) for k, v in event['OldResourceProperties'].items() if k != 'ServiceToken']) response = client.describe_instances(**old_params) print(response) SpotInstanceRequestId = response['Reservations'][0]['Instances'][0]['SpotInstanceRequestId'] response = client.cancel_spot_instance_requests( SpotInstanceRequestIds=[ SpotInstanceRequestId, ], ) print(response) response = client.describe_instances(**params) print(response) SpotInstanceRequestId = response['Reservations'][0]['Instances'][0]['SpotInstanceRequestId'] responseData = {} responseData['SpotInstanceRequestId'] = SpotInstanceRequestId cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, SpotInstanceRequestId) except Exception as e: print(e) cfnresponse.send(event, context, cfnresponse.FAILED, {}) Handler: index.handler MemorySize: 128 Role: !GetAtt AmazonEC2FullAccessRoleforLambda.Arn Runtime: python3.6 Timeout: 120 AmazonEC2FullAccessRoleforLambda: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: sts:AssumeRole Path: / ManagedPolicyArns: - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" - "arn:aws:iam::aws:policy/AmazonEC2FullAccess" CancelSpotInstanceRequests: Type: Custom::CancelSpotInstanceRequests Properties: ServiceToken: !GetAtt CancelSpotInstanceRequestsLambda.Arn InstanceIds: - !Ref EC2Instance Outputs: EC2Instance: Value: !Ref EC2Instance SpotInstanceRequestId: Value: !GetAtt CancelSpotInstanceRequests.SpotInstanceRequestId