この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
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