AWS STSのAssumeRoleを利用してS3にアップロードしてみる

AWS STSのAssumeRoleを利用して一時的セキュリティ認証情報を取得してS3にアップロードしてみたので内容をメモしておきます。
なお、[IAMロール徹底理解 〜 AssumeRoleの正体 ｜ Developers.IO](http://dev.classmethod.jp/cloud/aws/iam-role-and-assumerole/)がロールを理解するためにとても参考になります。

# 一時的セキュリティ認証情報について

一時的なセキュリティ認証情報は利用期限があるAWSを利用するための一時的な認証情報である。
一時的であるのでたとえ外部に漏れたとしても時間がたてば使えなくなる。
アプリに埋め込みされた認証情報が漏れる場合に比べて安全になる。


# ロールの継承とフェデレーショントークンの取得について

今回行ったのはロールの継承だが、以下のようなフェデレーショントークンの取得でも一時的セキュリティ認証情報は取得できる。

 * [AWS - 一時的セキュリティ認証情報を使って、特定のS3バケットにアップロードする - Qiita](http://qiita.com/arukoh@github/items/1b0c7e2a15a49ae517be)
 * [Ruby - AWS STSで期限付きのサーバ認証トークン発行 - Qiita](http://qiita.com/ogawatti/items/64173de948a0d01182f6)

理解不足で怪しい部分はあるが両者比較すると、ロールの継承ではロールの権限を利用して、フェデレーショントークンの取得ではIAMユーザの権限を利用する。

ロールの継承に比べフェデレーショントークンの取得での一時的セキュリティ認証情報有効期限は長い。
(ロールの継承：15分〜1時間、フェデレーショントークンの取得：1時間〜36時間)

フェデレーショントークンの取得ではIAMユーザのクレデンシャルが必要だが、AssumeRoleではクレデンシャルの保持が不要にできる(一時的セキュリティ認証情報で一時的セキュリティ認証情報を取得できる)。

# 実験内容

以下のCloudFormationのテンプレートを実行することでAssumeRoleを利用して作成したS3にファイルをアップロードする。
※ ap-northeast-1リージョンしか対応していない

この例だとEC2Roleにアップロード権限を与えてしまえば良いのでAssumeRoleする必要性が感じられないかもしれないが、一時的セキュリティ認証情報をクライアントアプリなどの別アプリに渡して利用させる場合に別アプリがIAMユーザのクレデンシャルなどを保持しなくて良くなる。

```json
{                    
  "AWSTemplateFormatVersion" : "2010-09-09",

  "Description" : "Sample Template For Assume Role",

  "Parameters" : {
    "KeyName" : {
      "Type" : "AWS::EC2::KeyPair::KeyName",
      "Description" : "Specified EC2 KeyName"
    }
  },

  "Mappings" : {
    "RegionMap" : {
      "ap-northeast-1" : { "PV" : "ami-2385b022" }
    }
  },

  "Resources" : {
    "S3PrivateBucket" : {
      "Type" : "AWS::S3::Bucket",
      "Properties" : { "AccessControl" : "Private" }
    },

    "EC2Role" : {
      "Type" : "AWS::IAM::Role",
      "Properties" : {
        "Path" : "/",
        "AssumeRolePolicyDocument" : {
          "Version" : "2012-10-17",
          "Statement" : [ {
            "Effect" : "Allow",
            "Principal" : { 
              "Service" : [ "ec2.amazonaws.com" ]
            },
            "Action" : [ "sts:AssumeRole" ]
          } ]
        },
        "Policies" : [ { 
          "PolicyName" : "sts-policy",
          "PolicyDocument" : {
            "Version" : "2012-10-17",
            "Statement" : [ {
              "Effect" : "Allow",
              "Action" : [ "sts:AssumeRole" ],
              "Resource" : "*"
            } ]
          }
        } ]
      }
    },

    "EC2Profile" : {
      "Type" : "AWS::IAM::InstanceProfile",
      "Properties" : { 
         "Path" : "/", 
         "Roles" : [ { "Ref" : "EC2Role" } ]
      }
    },

    "RoleForAssume" : {
      "Type" : "AWS::IAM::Role",
      "Properties" : {
        "Path" : "/",
        "AssumeRolePolicyDocument" : {
          "Version" : "2012-10-17",
          "Statement" : [ {
            "Effect" : "Allow",
            "Principal" : {
              "AWS" : { "Fn::GetAtt" : [ "EC2Role", "Arn" ] }
            },
            "Action" : [ "sts:AssumeRole" ]
          } ]
        },
        "Policies" : [ { 
          "PolicyName" : "s3-policy",
          "PolicyDocument" : {
            "Version" : "2012-10-17",
            "Statement" : [ {
              "Effect" : "Allow",
              "Action" : [ "s3:PutObject", "s3:PutObjectAcl" ],
              "Resource" : { "Fn::Join" : [ "", [ "arn:aws:s3:::", { "Ref" : "S3PrivateBucket" }, "/*" ] ] }
            } ]
          }
        } ]
      }
    },

    "EC2SecurityGroup" : {
      "Type" : "AWS::EC2::SecurityGroup",
      "Properties" : {
        "GroupDescription" : "ssh only",
        "SecurityGroupIngress" : [{ 
          "IpProtocol" : "tcp",
          "FromPort" : "22",
          "ToPort" : "22",
          "CidrIp" : "0.0.0.0/0"
        }]   
      }    
    },   

    "EC2" : {
      "Type" : "AWS::EC2::Instance",
      "Properties" : {
        "ImageId" : { "Fn::FindInMap" : [ "RegionMap", { "Ref" : "AWS::Region" }, "PV" ] }, 
        "InstanceType" : "t1.micro",
        "IamInstanceProfile" : { "Ref" : "EC2Profile" },
        "KeyName" : { "Ref" : "KeyName" },
        "SecurityGroups" : [ { "Ref" : "EC2SecurityGroup" } ], 
        "UserData" : { "Fn::Base64" : { "Fn::Join" : ["", [
          "#!/bin/bash -ex", "\n",
          "yum install -y rubygems ruby-devel gcc libxml2-devel libxslt-devel git", "\n",
          "gem install bundler --no-rdoc --no-ri", "\n",
          "cd /tmp", "\n",
          "git clone https://gist.github.com/37141d0dae07f481e473.git s3up_by_assume_role", "\n",
          "cd s3up_by_assume_role", "\n",
          "/usr/local/bin/bundle config build.nokogiri --use-system-libraries", "\n",
          "/usr/local/bin/bundle install --path vendor/bundle", "\n",
          "/usr/local/bin/bundle exec ruby s3up_by_assume_role.rb --region ", { "Ref" : "AWS::Region" }, " ", 
          "--s3-object-key hoge ",
          "--backet ", { "Ref" : "S3PrivateBucket" }, " ", 
          "--role-arn ", { "Fn::GetAtt" : [ "RoleForAssume", "Arn" ] }, "\n",
          "" ]]}} 
      }    
    }    
  },

  "Outputs" : {
    "UploadedURL" : {
      "Value" : { "Fn::Join" : [ "", [ "https://", { "Ref" : "S3PrivateBucket" }, ".s3.amazonaws.com/hoge" ] ] }                                                                                    
    }    
  }
}
```

## 解説

### テンプレートの概要

![assume-role-test.png](https://qiita-image-store.s3.amazonaws.com/0/16768/6ee1ed00-6e29-c31b-d57e-4573dc824fc4.png)

EC2に割立てるIAMロールEC2RoleはAssumeRoleの権限しかないが、S3にアップロードする権限があるIAMロールRoleForAssumeを利用することで作成したS3PrivateBucketにアップロードすることができる。

RoleForAssumeのAssumeRolePolicyDocumentでRoleForAssumeがEC2Roleから利用されることを許可している(Management ConsoleではRoleを選択してEdit Trust Relationshipで変更可能な部分)
ちなみに、EC2RoleのAssumeRolePolicyDocumentではEC2サービスからの利用を許可しているのでEC2に割り当てられる。

### cloud-initでのS3アップロード処理

S3へのアップロード処理は作成したEC2のcloud-initで以下のように実行している。

```bash
#!/bin/bash -ex
yum install -y rubygems ruby-devel gcc libxml2-devel libxslt-devel git
gem install bundler --no-rdoc --no-ri
cd /tmp
git clone https://gist.github.com/37141d0dae07f481e473.git s3up_by_assume_role
cd s3up_by_assume_role
/usr/local/bin/bundle config build.nokogiri --use-system-libraries
/usr/local/bin/bundle install --path vendor/bundle
/usr/local/bin/bundle exec ruby s3up_by_assume_role.rb --region [リージョン] --s3-object-key hoge --backet [作成したS3PrivateBucket名] --role-arn [作成したRoleForAssumeのARN]
```

はまったところ：
/usr/local/bin/bundleとフルパスなのは/usr/local/binにパスが通っていないから。
最初は気付かずにしばらくcloud-init処理で127(コマンドがない)で失敗し続けた。

### s3アップロードスクリプト

cloud-initでは以下のrubyスクリプトを利用している。

* https://gist.github.com/mechamogera/37141d0dae07f481e473

```rb
require 'aws-sdk'
require 'optparse'
 
options = {}
OptionParser.new do |opt|
  opt.on('-r', '--region=VALUE', 'リージョン') { |v| options[:region] = v }
  opt.on('-b', '--backet=VALUE', 's3バケット名') { |v| options[:backet] = v }
  opt.on('-a', '--role-arn=VALUE', 'IAMロールのARN') { |v| options[:role_arn] = v }
  opt.on('-k', '--s3-object-key=VALUE', 'アップロードするオブジェクトのキー') { |v| options[:s3_object_key] = v }
  opt.parse!(ARGV)
end
 
AWS.config(region: options[:region])
bucket_name = options[:backet]
object_name = options[:s3_object_key]
# 権限を特定のオブジェクトキーに絞る(別のキーにアップロードしたりすると権限がなくてエラーになる)
upload_policy = AWS::STS::Policy.new do |policy|
  policy.allow( actions: ['s3:PutObject', 's3:PutObjectAcl'], resources: "arn:aws:s3:::#{bucket_name}/#{object_name}")
end
# AssumeRoleで一時的なセキュリティ認証情報取得
upload_session = AWS::STS.new.assume_role(role_arn: options[:role_arn],
role_session_name: "test", policy: upload_policy.to_json)
 
#AWS::STS.new.new_federated_session("user_name", policy: upload_policy)
# => これは失敗する(EC2のRole権限なので)

# 一時的なセキュリティ認証情報を取得してS3アップロード(本来は認証情報を渡された別アプリで実施)
s3 = AWS::S3.new(upload_session[:credentials])
bucket = s3.buckets[bucket_name]
object = bucket.objects[object_name].write(file: __FILE__, acl: :public_read) 
```