ボクココ

サービス開発を成功させるまでの歩み

Rails でのシンプルな S3 ダイレクトアップロード実装

ども、@kimihom です。

f:id:cevid_cpp:20200312211753j:plain

今回はシンプルに S3 にファイルを上げる方法を案内しよう。

Rails 側でファイルアップロードを受ける課題

おそらく多くの Rails デベロッパーは、 画像のサイズ縮小や変換をするプログラムを実行したいがために、一度 Rails 側でファイルを受け取る実装をしたことがあることだろう。carrierwave などはまさにそのための Gem だ。

Webサイトからファイルアップロード => Rails アプリ => 画像等の変換 => S3

しかし時代は流れ、S3 にアップロードされたら、それをトリガーとして AWS Lambda 側でプログラムを実行させることが簡単にできるようになった。AWS Lambda 側でアップロードされた画像を取得・加工し、バックグラウンドジョブ的な形で変換されたファイルを S3 へ置きにいくことができる。その方が、明らかに Rails サーバー的には嬉しい。なぜなら、carrierwave などを使った画像の加工や生成には非常に多くのメモリや処理実行時間がかかり、サーバー負荷の代表的な要因であるからである。

Rails アプリ側では、そもそもアップロードされたファイルを受け取らない。ダイレクトアップロードを実装するべき価値はここにある。

Webサイトからファイルアップロード => S3 => AWS Lambda の実行

ということで、本記事では最もシンプルな Rails を使った S3 ファイルダイレクトアップロードの方法を解説する。

Presigned URL

今回の記事でキーワードとなるのが、S3 の Presigned という概念だ。

AWS 側で短期間有効な URL を生成し、その URL に対して ファイル付きの Ajax POST をすることでアップロードができる。この Presigned URL を生成するのに、Rails サーバー側で AWS SDK を利用する必要がある。以下に Presigned URL を生成するサンプルコードを示す。固定文字でのサンプルコードなので、適宜 動的なプログラムに改善して欲しい。

def presigned_post
    bucket = Aws::S3::Resource.new.bucket("my-bucket")
    post = bucket.presigned_post(
      key: "images/sample.jpg",
      acl: 'public-read',
      content_type: "image/jpg"
    )
    render json: { status: "ok", url: post.url, fields: post.fields }
end

まずダイレクトアップロード前に、この presigned_post アクションを Ajax 経由で呼び出そう。うまく実行されれば urlfilelds がそれぞれレスポンスとして返ってくる。この情報を用いて、フロントエンドでダイレクトアップロードを実行する。

var formdata = new FormData();
for (field in data.fields) {
  formdata.append(field, data.fields[field]);
}
formdata.append("file", file);
$.ajax({
  url: data.url,
  data: formdata,
  processData: false,
  contentType: false,
  method: "POST",
}).done(function() {
  console.log("uploaded");
}).fail(error);

presigned_post アクションで返ってきたデータに加えて、実際にアップロードしたいファイルを formdatafile として追加している。

なんということでしょう、これだけで S3 にダイレクトアップロードを実装できる!しかもフロントエンドで AWS-SDK の JavaScript を入れる必要もなく、単なる Ajax 呼び出しだけで完結してしまう。とても便利なものである。

S3 からファイルのダウンロード

パブリックとして公開していない S3 からファイルをダウンロードするのも同様に簡単に実装できるので紹介しておく。この場合、一時的な presigned_url を Rails 側で生成するだけでよくなる。

def presigned_get
    signer = Aws::S3::Presigner.new
    url = signer.presigned_url(:get_object, bucket: "my-bucket", key: "images/sample.jpg")
    render json: {url: url}
end

返ってきた URL に対して アクセスが可能になる。

$.ajax({
  url: data.url,
  processData: false,
  contentType: false
}).done(function(data) {
  console.log(data);
}).fail(error);

CORS の設定をお忘れなく

S3 側で アクセス権限 > CORS の設定をしよう。

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

終わりに

今回は S3 ダイレクトアップロード と おまけのダウンロードをご紹介した。

Rails ファイルアップロードによる高負荷処理から卒業しよう。これからはそうした処理は AWS Lambda に任せる。役割分担をしっかりとして、より効率的なサーバー運用を実現しよう。