LoginSignup
10
14

More than 5 years have passed since last update.

Rails + JavaScriptでクライアントから直接S3にファイルアップロード

Last updated at Posted at 2017-03-06

Railsアプリで1GB以上のファイルをS3にアップロードする際に、普通のフォームアップロードだと504 Time outエラーになるため、Railsアプリでは認証だけして、クライアント側で分割して、直接S3にアップロードするようにました。
その過程で、かなりの期間どはまりしたので、誰かの役に立てばと思いメモを残しました。

ただ、正直かなり色々なサイトを参考にしたのですが、どこも断片的な情報しかなかった上に、STSとかIAMとか完全に理解していないので、とりあえず成功した手順だけ記載します。

成功した手順

前提:Railsで利用するIAMユーザ(以下、Rails用ユーザ)が作成されている
1. AWS側でIAMのroleを設定する
2. AWS側でS3のバケットのCORS設定をする
3. RubyでSTSを利用して一時的な認証情報を取得する
4. JavaScriptで取得した認証情報を元にアップロードする

以下、各手順の詳細を記述します。

1. AWS側でIAMのroleを設定する

正直、ここが一番わからなかったです。出来て見れば、ロールとユーザ相互に権限設定しただけなのですが、それがわかるまでかなりはまりました。

やり方としては、「IAM > ロール > 新しいロールの作成」でAmazonS3FullAccessのアクセス権を持つロールを作成します。
そして、ロールの詳細画面で「信頼関係 > 信頼関係の編集」で下記のようにRails用ユーザを追加します。
ちなみに、変えていいのはAWS属性のユーザ名のみ。Versionの日付は固定なので、変更しないこと。
また、ユーザのARNの値はユーザの詳細画面に表示されています。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::123456789012:user/rails_user"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

次に、IAMのユーザ一覧でRails用ユーザの詳細を開き、「アクセス権限 > インラインポリシー > インラインポリシーの追加」で下記のようにインラインポリシーを追加します。
ここでも変えていいのはResourceのロール名のみ。ロールのARNの値もロールの詳細画面に表示されています。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "sts:AssumeRole",
            "Resource": "arn:aws:iam::012345678901:role/rails_source"
        }
    ]
}

2. AWS側でS3のバケットのCORS設定をする

「S3 > アクセス権 > アクセスコントロールリスト」で「Any authenticated AWS user」に書き込み権限を与えます。
次に、「S3 > アクセス権 > CORS設定」で下記のようにCORS設定を変更します。
色々なサイトに「AllowedMethodを追加する」ってことは書いてありましたが、ExposeHeaderを追加しないと、分割アップロードした時にETagが取れなくてエラーになりますので、注意してください。

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <CORSRule>
    <AllowedOrigin>*</AllowedOrigin>
    <AllowedMethod>GET</AllowedMethod>
    <AllowedMethod>PUT</AllowedMethod>
    <AllowedMethod>POST</AllowedMethod>
    <MaxAgeSeconds>3000</MaxAgeSeconds>
    <ExposeHeader>ETag</ExposeHeader>
    <AllowedHeader>*</AllowedHeader>
  </CORSRule>
</CORSConfiguration>

3. RubyでSTSを利用して一時的な認証情報を取得する

STSは「Security Token Service」の略で一時的な認証情報(token)を発行してくれるサービスです。
今回はassume_roleという一時的にセッションをあるロールを付与したとみなしてAWSを操作出来る認証方法を利用しました。
これ以外にも、get_federation_tokenなどでも同じ様なことが出来ると思います。調べるうちにcognitoなんてサービスもありましたが、これはユーザ認証を代行してくれるサービスなんですかね。利用しなかったのでわかりませんが。

具体的には下記のようなコントローラのメソッドでtokenを取得して、ブラウザに返すようにしました。
AWS_KEYはrails用ユーザの詳細画面の認証情報タブで作成したアクセスキーID、AWS_SECRETはそのパスワード。

ここで、assume_roleで指定するrole_arnにユーザを指定してみたりしてかなりはまりました。
ユーザを指定するとここではエラーが出ませんが、ファイルアップロード時にAccess Deniedになりました。

token_controller.rb
 def get_aws_token
   sts = Aws::STS::Client.new(
           region: 'ap-northeast-1',
           access_key_id: AWS_KEY,
           secret_access_key: AWS_SECRET
         )
   policy = JSON.generate(
              'Version' => '2012-10-17',
              'Statement' => [{
                'Effect' => 'Allow',
                'Action' => 's3:PutObject',
                'Resource' => 'arn:aws:s3:::bucket_name/folder_name/*'
              }]
            )
   res = sts.assume_role({
     role_session_name: 'some_session',
     role_arn: 'arn:aws:iam::012345678901:role/rails_source',
     policy: policy
   })
   render json: {assume_role: res}
 end

4. JavaScriptで取得した認証情報を元にアップロードする

まず、viewでaws-sdkをインクルードしておく。

view.html.erb
<%= javascript_include_tag 'https://sdk.amazonaws.com/js/aws-sdk-2.7.20.min.js', 'danta-turbolinks-track': 'reload' %>

最後に、アップロードボタンをクリックしたらtokenを取得して、成功したらAWSにアップロードするようにする。
AWS.S3.uploadは特に何もしなくても、巨大ファイルは分割してアップロードしてくれる。
ここでも、session_tokenをセットしないとエラーになるなど、credentialの渡し方でかなりはまった。

upload.js
$("#upload_button").click(function() {
  $.ajax({
    type: "GET",
    url: "/token/get_aws_token",
    success: function(res) {
      var token = res.assume_role.credentials;
      AWS.config.region = "ap-northeast-1";
      AWS.config.credentials = new AWS.Credentials({
        sessionToken: token.session_token,
        secretAccessKey: token.secret_access_key,
        accessKeyId: token.access_key_id
      }); 
      var file = $("#video_file")[0].files[0];
      var bucket = new AWS.S3({params: {Bucket: "bucket_name"}});
      var params = {Key: "folder_name/" + file.name, ContentType: file.type, Body: file};
      bucket.upload(params, function (err, data) {
        console.log(err); // エラーがあれば表示される
      }).on('httpUploadProgress', function(evt) {
        console.log("完了率:" + parseInt((evt.loaded * 100) / evt.total) + '%');
      });
    }
  });
}); 

補足

  • STSを呼ぶ際に、NTPでrailsアプリの時間を整えておかないと、tokenの有効期限がおかしなことになってエラーになります。
10
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
14