Railsアプリで1GB以上のファイルをS3にアップロードする際に、普通のフォームアップロードだと504 Time outエラーになるため、Railsアプリでは認証だけして、クライアント側で分割して、直接S3にアップロードするようにました。
その過程で、かなりの期間どはまりしたので、誰かの役に立てばと思いメモを残しました。
ただ、正直かなり色々なサイトを参考にしたのですが、どこも断片的な情報しかなかった上に、STSとかIAMとか完全に理解していないので、とりあえず成功した手順だけ記載します。
成功した手順
前提:Railsで利用するIAMユーザ(以下、Rails用ユーザ)が作成されている
- AWS側でIAMのroleを設定する
- AWS側でS3のバケットのCORS設定をする
- RubyでSTSを利用して一時的な認証情報を取得する
- 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になりました。
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をインクルードしておく。
<%= 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_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の有効期限がおかしなことになってエラーになります。