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


# 発端

オンプレミス環境で 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する５分前に取り直しをします。

# 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!