IAM Roleの仕組みを追う – なぜアクセスキーを明記する必要がないのか
はじめに
こんにちは。望月です。
過去に本ブログで、IAM Roleの仕組みについて都元から解説がありました。 - IAMロール徹底理解 〜 AssumeRoleの正体
IAM Roleの仕組みについて非常にわかりやすく解説されていますので、ぜひ読んでみてください。今日はもう少し利用側の観点に立ったブログを書いてみようと思います。
IAM Roleってどうやって使われてるの
IAM Roleを利用する目的は、「ソースコード内にAWSのAPIキーをハードコードすることなく、AWSのAPIを叩きたい」というものが殆どだと思います。ですが、なぜIAM Roleを利用すると、アクセスキーをソースコードで指定することなくAWSのAPIが利用可能になるのでしょうか。
今日はその仕組みについて知りたくなったので、AWS SDK for Rubyのソースコードから読み解いてみました。SDKのバージョンは1.40.0です。
CredentialProvider
アクセスキーの取得はAWS::Core::CredentialProvidersモジュールに属するクラスによって実行されます。そして、AWS::Core::Configurationのcredential_providerによって、実際にどのクラスをCredentialProviderとして利用するかを設定します。(参考 : AWS SDK for Ruby Documentation : Class: AWS::Core::Configuration)初期設定では、DefaultProviderを利用するようなので、該当部分のソースコードを読んでみます。
lib/aws/core/credential_providers.rbの110-123行目あたりです。
class DefaultProvider include Provider # (see StaticProvider#new) def initialize static_credentials = {} @providers = [] @providers << StaticProvider.new(static_credentials) @providers << ENVProvider.new('AWS') @providers << ENVProvider.new('AWS', :access_key_id => 'ACCESS_KEY', :secret_access_key => 'SECRET_KEY', :session_token => 'SESSION_TOKEN') @providers << ENVProvider.new('AMAZON') @providers << SharedCredentialFileProvider.new if Dir.home rescue ArgumentError @providers << EC2Provider.new end
アクセスキーとシークレットキーの取得元を、以下の順序で確認していきます。
- StaticProvider : AWS.configで指定したアクセスキーとシークレットキーを利用する
- ENVProvider : 環境変数からキーを取得する。引数は環境変数のPrefixとSuffix。
- SharedCredentialProvider : SharedCredentialファイル。AWS CLIでよく使うやつですね。デフォルトは(~/.aws/credentials)
- EC2Provider : EC2のIAM Roleから取得する
実際にどのProviderを利用するかは、AWS::Core::CredentialProviders::DefaultProvider#credentialsの結果から判断されます。上記の順番で参照していき、一番最初にアクセスキーとシークレットキーを取得することが出来たProviderを利用します。どのProviderからもアクセスキーとシークレットキーが取得出来なかった場合、MissingCredentialsErrorが投げられます。
同じくlib/aws/core/credential_providers.rbの128行目あたりです。
def credentials providers.each do |provider| if provider.set? return provider.credentials end end raise Errors::MissingCredentialsError end
130行目のprovider.set?の部分で、各Providerから実際に取得可能かどうかを判定しています。
def set? @cache_mutex ||= Mutex.new unless @cached_credentials @cache_mutex.synchronize do @cached_credentials ||= get_credentials end end @cached_credentials[:access_key_id] && @cached_credentials[:secret_access_key] end
Provider#setの中で、Provier::get_credentialsを実行してアクセスキーとシークレットキーの取得が成功した場合、その結果を@cached_credentialsとして保存します。
ここまでソースコードを読んで、実際にアクセスキーとシークレットキーを取得するのは各Providerのget_credentialsメソッドだということがわかりました。
EC2Provicder
それでは今回の目的である、EC2Providerを読んでいきます。まずはEC2Provider#initializeです。
lib/aws/core/credential_providers.rbの347行目-353行目です。
def initialize options = {} @ip_address = options[:ip_address] || '169.254.169.254' @port = options[:port] || 80 @http_open_timeout = options[:http_open_timeout] || 1 @http_read_timeout = options[:http_read_timeout] || 1 @http_debug_output = options[:http_debug_output] end
各種初期値の設定ですね。ここには記載しませんが、それぞれattr_accessorが定義されています。
上述の通り、アクセスキーを取得するのはEC2Provider#credentialになるので、そこを読んでみます。391行目あたりです。
def get_credentials begin http = Net::HTTP.new(ip_address, port) http.open_timeout = http_open_timeout http.read_timeout = http_read_timeout http.set_debug_output(http_debug_output) if http_debug_output http.start # get the first/default instance profile name path = '/latest/meta-data/iam/security-credentials/' profile_name = get(http, path).lines.map(&:strip).first # get the session details from the instance profile name path << profile_name session = JSON.parse(get(http, path)) http.finish credentials = {} credentials[:access_key_id] = session['AccessKeyId'] credentials[:secret_access_key] = session['SecretAccessKey'] credentials[:session_token] = session['Token'] @credentials_expiration = Time.parse(session['Expiration']) credentials rescue *FAILURES => e {} end end
http://169.254.169.254/latest/meta-data/iam/security-credentials/にリクエストを投げています。このパスにHTTPリクエストを投げると、IAM Role Nameが取得できるので、それをHTTPリクエストパスに加えて、再度リクエストを投げています。
例えば、default-roleというIAM RoleをEC2に対して割り当てている場合、クレデンシャルを取得するための最終的なリクエストパスは
http://169.254.169.254/latest/meta-data/iam/security-credentials/default-roleとなります。
実際に、私の手元でEC2を立ち上げて確かめてみました。
$ curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ default-role $ curl http://169.254.169.254/latest/meta-data/iam/security-credentials/default-role { "Code" : "Success", "LastUpdated" : "2014-07-05T06:30:19Z", "Type" : "AWS-HMAC", "AccessKeyId" : "ASIAxxx...", "SecretAccessKey" : "xxxxx........", "Token" : "xxxxxxxxxxxxxxxxxxxxx...", "Expiration" : "2014-07-05T13:05:33Z" }
各種クレデンシャルが記載されたJSONが返ってきました。SDKでは、AccessKeyId, SecretAccessKey, Token, Expirationをそれぞれ取得して、credentialとして呼び出し元に返却しています。
ここで取得されたAccessKeyが、APIリクエスト(aws/core/client.rb)や署名(aws/core/signers/*)などで利用されています。
まとめ
今回はAWS SDK for Rubyのソースコードから、IAM Roleを利用した時のアクセスキーの取得方法を調査しました。
AWS SDK for Rubyの場合は、
- ハードコードされたAPIキー
- 環境変数
- SharedCredentialファイル
- IAM Role(EC2 metadata)
の順で取得を試みます。RubyのSDKしか読んでいませんので、他言語のSDKでは順序や取得方法が異なっているかもしれません。
個人的には、SharedCredentialファイルを利用できるのを知らなかったので、少し驚きました。以前のブログでRubyのスクリプトから--profileオプションを利用するための仕組みを自前で実装してしまいましたが、普通にSDKの仕組みを利用して扱えそうですね。それに関しては別の機会にブログを書こうと思います。