AWS SDK for Ruby V2使ってみたけどまだ辛かった

そういえばAWS SDK for RubyのV2が出てたけどまだ試してないなぁ、
と思って手元のEC2インスタンス作成スクリプトをV2に書き換えてみたら辛かったです。

V1

V2

日本語でのV2の紹介記事はクラスメソッドさんのブログにありました。

v2のコードは、

  • 基本機能が定義されたaws-sdk-core
  • 抽象化されたリソースクラスが定義されたaws-sdk-resources

という2つのgemに分かれています。

aws-sdk-coreはstableですが、aws-sdk-resourcesはまだpreviewです。

じゃあまぁ、

  • aws-sdk-coreだけでどの程度使えるのか
  • aws-sdk-resourcesがどの程度preview版なのか

というところが気になるところです。

スクリプト仕様

こんな感じでスクリプトをたたくと、

$ export AWS_ACCESS_KEY_ID=XXX
$ export AWS_SECRET_ACCESS_KEY=XXX
$ ./bin/ec2_create_instance.rb v2-test01 ec2_default t2.micro

このあたりが実行されて

  • EC2インスタンスの作成
  • EIPの取得とアタッチ
  • インスタンス、EBSボリュームにNameタグを設定
  • Route53でホスト名とEIPの外部ドメイン名をCNAMEでヒモ付け

すぐにSSHでログインして作業できる。(実際はまずknife soloを実行する)

$ ssh ec2-user@v2-test01.mikeda.jp

       __|  __|_  )
       _|  (     /   Amazon Linux AMI
      ___|\___|___|

https://aws.amazon.com/amazon-linux-ami/2014.09-release-notes/
18 package(s) needed for security, out of 42 available
Run "sudo yum update" to apply all updates.
[ec2-user@v2-test01 ~]$ 

スクリプトの2つ目の引数はrole的なもので、外部のYAMLファイルに定義しています。

default: &default
  region: ap-northeast-1
  vpc_id: vpc-4c6f2825
  subnet_id: subnet-456f282c # ap-northeast-1b

ec2_default: &ec2_default
  <<: *default
  image_id: ami-4985b048 # Amazon Linux AMI 2014.09.1 (HVM)
  security_group_ids:
    - sg-d65d42ba # default
    - sg-1454ac71 # ssh-login
  key_name: "mikeda"
  hosted_zone_id: Z3J5MVVWBVNX6B # mikeda.jp
  iam_instance_profile: "default"
  ebs_size: 30

xxx_app:
  <<: *ec2_default
  ebs_size: 50
  iam_instance_profile: xxx-app

...

まぁこのへんは今回の話にはあんま関係ないです。

既存のv1スクリプト

こんな感じでした。
共通の前処理は省略しているので、全体を確認したい場合はここを見て下さい。

#!/usr/bin/env ruby

require 'aws-sdk-v1'

### 引数取得、設定ファイル読み込み、user_data作成など
### 共通処理なので省略

ec2 = AWS::EC2.new

### インスタンス作成
instance = ec2.instances.create(
  image_id:        config['image_id'],
  instance_type:   instance_type,
  key_name:        config['key_name'],
  subnet:          config['subnet_id'],
  user_data:       user_data,
  security_group_ids: config['security_group_ids'],
  iam_instance_profile: config['iam_instance_profile'],
  block_device_mappings: [
    {
      device_name: '/dev/xvda',
      ebs: { volume_size: config['ebs_size'].to_i }
    }
  ]
)

while instance.status != :running
  puts "Launching instance #{instance.id}, status: #{instance.status}"
  sleep 5
end

### EIPのAllocateとAssociate
elastic_ip = ec2.elastic_ips.create(vpc: config['vpc_id'])
# なんかエラーになるのでちょっとsleep
sleep 5

instance.associate_elastic_ip(elastic_ip)
puts "associated EIP : #{elastic_ip.ip_address}"

### タグ設定
root_volume = instance.attachments['/dev/xvda'].volume

ec2.tags.create(instance,    'Name', value: hostname)
ec2.tags.create(root_volume, 'Name', value: "#{hostname}_root")

### Route53にレコード追加
r53 = AWS::Route53.new
hosted_zone = r53.hosted_zones[config['hosted_zone_id']]
fqdn = hostname + '.' + hosted_zone.name
hosted_zone.rrsets.create(
  fqdn,
  'CNAME',
  ttl: 300,
  resource_records: [
    { value: instance.public_dns_name}
  ]
)

操作には抽象化されたインタフェースを使い、
EC2インスタンス、EIP、Route53のHostedZone等はそれぞれのリソースを表すClassのインスタンスとして取得しています。

例えば、ec2.instancesはEC2に関する抽象化された操作をまとめたAWS::EC2::InstanceCollectionのインスタンスで、
ec2.instances.createの返り値はEC2インスタンスを表すAWS::EC2::Instanceのインスタンスです。

v2スクリプト

aws-sdk-coreのみを使って書くとこうなります。

#!/usr/bin/env ruby

require 'aws-sdk-core'
require 'base64'
require 'yaml'

### 引数取得、設定ファイル読み込み、user_data作成など
### 共通処理なので省略

ec2 = Aws::EC2::Client.new

### インスタンス作成
puts "run_instances"
response = ec2.run_instances(
  image_id:  config['image_id'],
  min_count: 1,
  max_count: 1,
  instance_type: instance_type,
  key_name: config['key_name'],
  subnet_id: config['subnet_id'],
  user_data: Base64.encode64(user_data),
  security_group_ids: config['security_group_ids'],
  iam_instance_profile: {
    name: config['iam_instance_profile']['name']
  },
  block_device_mappings: [
    {
      device_name: '/dev/xvda',
      ebs: { volume_size: config['ebs_size'].to_i }
    }
  ]
)

instance = response.instances.first

puts "wait until instance_running"
ec2.wait_until(:instance_running, instance_ids: [ instance.instance_id ]) do |w|
  w.interval = 5
  w.max_attempts = 20
  w.before_wait do |attempt, prev_response|
    instance = prev_response.reservations.first.instances.first
    puts "#{instance.instance_id} : #{instance.state.name}"
  end
end

### EIPの取得とアタッチ
eip = ec2.allocate_address(
  domain: "vpc",
)

ec2.associate_address(
  instance_id: instance.instance_id,
  allocation_id: eip.allocation_id
)

### インスタンスとEBSにタグ付け
ec2.create_tags(
  resources: [ instance.instance_id ],
  tags: [ { key: "Name", value: hostname } ]
)

ec2.create_tags(
  resources: [ instance.block_device_mappings.first.ebs.volume_id ],
  tags: [ { key: "Name", value: "#{hostname}_root" } ]
)

### Route53にCNAMEを登録
route53 = Aws::Route53::Client.new()
hosted_zone = route53.get_hosted_zone(id: config['hosted_zone_id']).hosted_zone
fqdn = hostname + '.' + hosted_zone.name
route53.change_resource_record_sets(
  hosted_zone_id: hosted_zone.id,
  change_batch: {
    changes: [
      action: 'CREATE',
      resource_record_set: {
        name: fqdn,
        type: 'CNAME',
        ttl: 300,
        resource_records: [
          { value: instance.public_dns_name }
        ]
      }
    ]
  }
)
puts "create DNS record : #{fqdn}"

『なんとか_id』が目白押し!

EC2の操作はAws::EC2::Clientクラスのオブジェクトで全て実行しています。
そして例えば、ec2.run_instancesの返り値はEC2インスタンスを表すクラスのオブジェクトじゃなく、ただのStructです。

[13] pry(main)> response
=> #<struct 
 reservation_id="r-9b024082",
 owner_id="518578968550",
 requester_id=nil,
 groups=[],
 instances=
  [#<struct 
    instance_id="i-dc1c18c5",
    image_id="ami-4985b048",
    state=#<struct  code=0, name="pending">,
    private_dns_name="ip-10-0-0-226.ap-northeast-1.compute.internal",
    public_dns_name="",
    state_transition_reason="",

EIPやタグの設定も各種IDをメソッドの引数として引き回して操作する。
APIそのままで、まぁダルいです。

じゃあaws-sdk-resourcesを使えばいいのか

aws-sdk-resourcesを使うとインスタンス作成部分はこんな感じで書けます。

require 'aws-sdk-resources'

### 引数取得、設定ファイル読み込み、user_data作成など
### 共通処理なので省略

ec2 = Aws::EC2::Resource.new

instances = ec2.create_instances(
  image_id:  config['image_id'],
  min_count: 1,
  max_count: 1,
  instance_type: instance_type,
  key_name: config['key_name'],
  subnet_id: config['subnet_id'],
  user_data: Base64.encode64(user_data),
  security_group_ids: config['security_group_ids'],
  iam_instance_profile: {
    name: config['iam_instance_profile']
  },
  block_device_mappings: [
    {
      device_name: '/dev/xvda',
      ebs: { volume_size: config['ebs_size'].to_i }
    }
  ]
)
instance = instances.first
instance.wait_until_running

操作にはAws::EC2::Clientではなく、Aws::EC2::Resourceのインスタンスを使っていて、ec2.create_instancesの返り値はAws::EC2::Instanceクラスのインスタンスです。
インスタンス起動まで待機するwait_until_runningみたいなメソッドもあってなかなかいい。

しかし、Aws::EC2::ElasticIPはまだ無いようで、
けっきょくEIPの設定には前述のAws::EC2::Clientを使わないといけない。

現状、EC2関連で定義されているリソースはこのあたり。

[2] pry(main)> Aws::EC2.constants
=> [:Client,
 :Errors,
 :Resource,
 :DhcpOptions,
 :Image,
 :Instance,
 :InternetGateway,
 :KeyPair,
 :NetworkAcl,
 :NetworkInterface,
 :PlacementGroup,
 :RouteTable,
 :RouteTableAssociation,
 :SecurityGroup,
 :Snapshot,
 :Subnet,
 :Tag,
 :Volume,
 :Vpc,
 :VpcPeeringConnection]

v1にあったこのへんは実装待ちということなのかな。

Attachment
AvailabilityZone
CustomerGateway
DHCPOptions
ElasticIp
ExportTask
Region
ReservedInstances
ReservedInstancesOffering

そしてRoute53のほうはというと、

[3] pry(main)> Aws::Route53.constants
=> [:Client, :Errors, :Resource]

HostedZoneもResourceRecordSetもなんもない(´・ω・`)

まとめ

AWS SDK for Ruby V2を使ってEC2インスタンスを作成してみました。

v2のコードは、

  • 基本機能が定義されたaws-sdk-core
  • 抽象化されたリソースクラスが定義されたaws-sdk-resources

という2つのgemに分かれているのですが、

  • aws-sdk-coreだけではコーディングがけっこう辛い
  • aws-sdk-resourcesはまだ未対応のリソースが多そう

というわけで今回は切り替えを見送りました。

切り替えを実施する場合は、使いそうなリソースがaws-sdk-resourcesに定義されているかをちゃんと確認したほうがいいかもです。