Edited at

なんちゃって metadata-server を作ってみた話

More than 3 years have passed since last update.


発端

オンプレミス環境で EC2 Instance Profile を使いたいというユースケースを聞きました。

SDKやCLIの中には、EC2 Instance Profileを利用するために、

http://169.254.169.254/latest/meta-data/iam/security-credentials/

http://169.254.169.254/latest/meta-data/iam/security-credentials/role名

へアクセスを行い、テンポラリのクレデンシャルを取得する仕組みが入っています。

このURLへのアクセスが出来れば、オンプレミス環境でもEC2 Instance Profileと同じような事が出来るんじゃないの?というのが発端で、実際に作ってみました。


コード

#!/usr/bin/env ruby

require 'sinatra'
require 'aws-sdk-core'

set :bind, '0.0.0.0'

role_arn=ARGV.last || ""
abort "usage: ruby ec2role.rb [ -p PORT ] arn:aws:iam:111111111111:role/ROLENAME" unless role_arn.match /^arn:aws:iam:/

sts = Aws::STS::Client.new(region: 'us-east-1')
json="n/a"

Thread.new do
loop do
begin
resp = sts.assume_role(role_arn:role_arn,role_session_name:"ec2role.rb")
rescue => error
abort "ERROR: unable to fetch credential / "+error.to_s
end
json= <<EOL
{
"Code" : "Success",
"AccessKeyId" : "#{resp.credentials.access_key_id}",
"SecretAccessKey" : "#{resp.credentials.secret_access_key}",
"Token" : "#{resp.credentials.session_token}",
"Expiration" : "#{resp.credentials.expiration.iso8601}"
}
EOL
puts json
sleep resp.credentials.expiration-Time.now-60*5 # update credential 5 mins before it expires
end
end

get '/latest/meta-data/iam/security-credentials/' do
'temp'
end

get '/latest/meta-data/iam/security-credentials/temp' do
json
end


使い方

まず、AssumeRoleを行うためのIAMユーザと、AssumeRoleされる側のRoleを作成します。

RoleはCross-Account access用のRoleを作成し、AssumeRoleを行うユーザを信頼します。

(同じアカウント同士であっても信頼する必要があります)

RubyのコードはIAMユーザの credential を使用して起動する必要があります。

クライアントにはLinuxを想定します(iptablesを使用するため)。

送信されるパケットのうち、169.254.169.254 宛のパケットを転送するために、

# /sbin/sysctl -w net.ipv4.ip_forward=1

# /sbin/iptables -A FORWARD -m tcp -p tcp --dst 169.254.169.254 --dport 80 -j ACCEPT
# /sbin/iptables -A FORWARD -m state --state ESTABLISHED,RELATED -j ACCEPT
# /sbin/iptables -t nat -A OUTPUT -m tcp -p tcp --dst 169.254.169.254 --dport 80 -j DNAT --to-destination 127.0.0.1:4567

みたいな感じで、パケットの宛先を書き換えてしまいます。

Rubyのコードを実行しているログが下記のようになります。

~/work/ec2role$ ruby ec2role.rb  arn:aws:iam::407613804811:role/power

== Sinatra/1.4.5 has taken the stage on 4567 for development with backup from Thin
Thin web server (v1.6.3 codename Protein Powder)
Maximum connections set to 1024
Listening on 0.0.0.0:4567, CTRL+C to stop
{
"Code" : "Success",
"AccessKeyId" : "ASIAxxxxxxxxxxxxxxxxxxxxxxx",
"SecretAccessKey" : "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"Token" : "hogehogehogehogehogehogehogehogehogehogehogehogehogehogehogehogehogehogehoge",
"Expiration" : "2014-12-02T10:26:26Z"
}
127.0.0.1 - - [02/Dec/2014:18:26:47 +0900] "GET /latest/meta-data/iam/security-credentials/ HTTP/1.1" 200 4 0.0052
127.0.0.1 - - [02/Dec/2014:18:26:47 +0900] "GET /latest/meta-data/iam/security-credentials/temp HTTP/1.1" 200 554 0.0005

起動時にテンポラリのクレデンシャルを取りに行き、クライアントからアクセスが来たら渡しています。

Expireする5分前に取り直しをします。


iptablesがない場合は?

SDKやCLIには、 http://169.254.169.254/ のアドレスが埋め込まれているので、それを書き換えてしまえば使えます。

AWS CLIでは、botocore/utils.py

METADATA_SECURITY_CREDENTIALS_URL = (

# 'http://169.254.169.254/latest/meta-data/iam/security-credentials/'
'http://localhost:4567/latest/meta-data/iam/security-credentials/'
)

みたいな感じです。


追記

某meet-upで、instance-id とかには対応できないの?というご意見をいただきました。 出来ます!!

sinatraでは ./public が static content として扱われるので、

~/work/ec2role$ mkdir -p public/latest/meta-data

~/work/ec2role$ echo -n i-12345678 > public/latest/meta-data/instance-id
~/work/ec2role$ echo hoge > public/latest/user-data

みたいにファイルを作っておけば、

$ curl http://169.254.169.254/latest/user-data

hoge
$ curl http://169.254.169.254/latest/meta-data/instance-id
i-12345678

127.0.0.1 - - [09/Dec/2014:15:44:00 +0900] "GET /latest/user-data HTTP/1.1" 200 20 0.0018

127.0.0.1 - - [09/Dec/2014:15:44:30 +0900] "GET /latest/meta-data/instance-id HTTP/1.1" 200 10 0.0009

みたいに返す事が可能です。

Have fun!