工場長のブログ

日々思ったことを書いてます。

EC2スポットインスタンスを固定IP付きで立ち上げる

EC2のAPIでは、オンデマンドインスタンス(リザーブドインスタンスも)は固定IP付きで立ち上げることができるが、スポットインスタンスはそういうAPIはない。

もともとスポットインスタンスは起動していることが保証されているわけではないので固定IPを付けて運用するようなケースはあまりないと思うが、そういうケースに出くわしたので考えてみた。ちなみに今回はVPCの中で動かすのが想定ケース。

ステップの概要としては

  1. 欲しいIPをつけたENIを作る
  2. スポットインスタンスAPIでリクエストする。このときにAPIのパラメータとしてuser-dataでアタッチしたいENIのIDを渡す
  3. 立ち上がってきたスポットインスタンスはuser-dataで渡されたENIをcloud-initのuser-scriptやinitスクリプトでアタッチする

pythonで書くとこんな感じ。
今回はcloud-initのuser-scriptを使って実装しました。

import json
import optparse
import urllib2
import sys

import boto.ec2

# parse args
parser = optparse.OptionParser()
parser.add_option('-e', '--eni_id', dest='eni_id')
parser.add_option('-r', '--iam_role', dest='iam_role')
parser.add_option('-t', '--target_iam_role', dest='target_iam_role')
parser.add_option('-a', '--iam_arn', dest='iam_arn')
parser.add_option('-p', '--price', dest='price')
parser.add_option('-k', '--key', dest='key')
parser.add_option('-s', '--sg_id', dest='sg_id')
parser.add_option('-i', '--ami', dest='ami')
parser.add_option('-n', '--subnet', dest='subnet')
options, reminder = parser.parse_args()

# create a ec2connection instance
response = urllib2.urlopen('http://169.254.169.254/latest/meta-data/iam/security-credentials/' + options.iam_role)
credential = json.loads(response.read())
ec2connection = boto.ec2.connect_to_region(
	'ap-northeast-1',
	aws_access_key_id=credential["AccessKeyId"],
	aws_secret_access_key=credential["SecretAccessKey"],
	security_token=credential["Token"]
)

# make a spot_instance request
user_data = "#!/bin/bash -ex \n python /usr/local/staticip_to_spotinstance_in_vpc/client.py -e" + options.eni_id + " -t " + options.target_iam_role
request_spot_instance = ec2connection.request_spot_instances(
	options.price,
	options.ami,
	user_data=user_data,
	key_name=options.key, 
	subnet_id=options.subnet,
	security_group_ids=[options.sg_id],
	instance_profile_arn=options.iam_arn
)

print request_spot_instance
  • そんで立ち上がってくるスポットインスタンス側には/usr/localの下にこんなの仕込んでおいて、起動後に実行される。パラメータとしてアタッチするENIのID(とこのサーバーのIAM ROLE)が渡ってくる。
import json
import optparse
import sys
import urllib2

import boto.ec2

# parse args
parser = optparse.OptionParser()
parser.add_option('-e', '--eni_id', dest='eni_id')
parser.add_option('-r', '--iam_role', dest='iam_role')
parser.add_option('-t', '--target_iam_role', dest='target_iam_role')
options, reminder = parser.parse_args()

# create ec2connection instance
response =
urllib2.urlopen('http://169.254.169.254/latest/meta-data/iam/security-credentials/'
+ options.target_iam_role)
credential = json.loads(response.read())
ec2connection = boto.ec2.connect_to_region(
	'ap-northeast-1',
	aws_access_key_id=credential["AccessKeyId"],
	aws_secret_access_key=credential["SecretAccessKey"],
	security_token=credential["Token"]
)

# attach eni
response_instance_id = urllib2.urlopen('http://169.254.169.254/latest/meta-data/instance-id')
instance_id = response_instance_id.read()
print ec2connection.attach_network_interface(options.eni_id, instance_id, 2)


ENIをアタッチするところで時々APIがタイムアウトするのを見かけたので、成功するまで何度かループしたほうが良かったかも。

あと、今回はSTS(Security Token Service)というのをAPIの認証に使ってみた。
IAMのROLEをインスタンスに割り振っておいて、そのROLEに対する、期限付きのACCESS_KEY, SECRET_KEY, TOKENがもらえてそれでAPIの認証ができるという仕組み。pythonのbotoからの使い方はいまいちちゃんとわからなかったのだけれども、EC2のメタデータサービスから下記の用にしてこれらのデータが取れることを確認

$ curl http://169.254.169.254/latest/meta-data/iam/security-credentials/IAM_ROME_NAME

レスポンスはこんなJSON

{
  "Code" : "Success",
  "LastUpdated" : "2012-10-08T05:20:14Z",
  "Type" : "AWS-HMAC",
  "AccessKeyId" : "期限付きACCESS_KEY",
  "SecretAccessKey" : "期限付きSECRET_KEY",
  "Token" : "期限付きTOKEN",
  "Expiration" : "2012-10-08T11:50:33Z"
}

AMIやコード、設定ファイルの中にCredential系の情報を埋め込まなくて済むのでいいですね。