ちょっと話題の記事

IAM Roleの仕組みを追う – なぜアクセスキーを明記する必要がないのか

2014.07.05

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

こんにちは。望月です。
過去に本ブログで、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::Configurationcredential_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の仕組みを利用して扱えそうですね。それに関しては別の機会にブログを書こうと思います。