AWS SDK S3 for Rubyを用いてAPIを叩き画像オブジェクトをダウンロードする

5 min読了の目安(約4700字TECH技術記事

※画像は『AWS SDK AWS Black Belt Tech Webinar 2015』のスライドから引用

微妙に沼ったので戒めを込めて書きます。zenn初カキコ...ども...
認証周りでたくさんアクセスディナーイされたのでとてもツラかった。

やりたいこと

  • S3バケット上に保存されている画像オブジェクトを、API経由でダウンロードする
    • 画像オブジェクトの指定には、S3の画像オブジェクトキー(getObjectKey)を指定する
    • AWS公式が用意しているSDK(aws-sdk)を使ってダウンロードを行う

https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html

動作環境

  • Ruby: 2.6.6
  • Rails: 5.2.3
  • aws-sdk-s3: 1.83.0
    • aws-sdk-core : 3.109.1

https://github.com/aws/aws-sdk-ruby

前提

  • Instance Profileを用いて、コードを動かしたいEC2に s3:GetObjectの権限を付与したIAM::Roleを設定
    • なので、Rubyコード上で access key idとかsecret access keyを管理して設定する必要がなかった
    • 裏を返すと、コード上にIAM::Roleを渡しているコンテキストが残りづらいので、細かくコメント書いたほうが良いように思う

https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_roles_use_switch-role-ec2_instance-profiles.html

コード

  • 以下のコードを実行すると、 local_pathの名前で画像がダウンロードされます
# RailsでGem使って動かす場合は不要
require 'aws-sdk-s3'

# AWSのconfigを設定
s3_client = Aws::S3::Client.new(region: 'ap-northeast-1')
bucket_name = 'hogehoge'
# オブジェクトキーとダウンロード先を指定(後ほど解説)
object_key = 'huga.jpg'
local_path = "./#{object_key}"
 
# APIを叩き画像をダウンロードする
s3_client.get_object(
    response_target: local_path,
    bucket: bucket_name,
    key: object_key
 )

少し解説

local_pathの名前で画像がダウンロードされます

ここが気持ち悪いよねわかる。言い訳させてくれ。

画像オブジェクトの指定の仕方について

そもそもここが抜けてたんですが、S3ってフォルダの概念がないんですよね(誰がどう見てもフォルダやん...)。んで、フォルダで言うところのファイル名の部分がS3のオブジェクトキーにあたる。

  • フルパス: hoge/huga.txt
  • オブジェクトキー: huga

https://dev.classmethod.jp/articles/amazon-s3-folders/

一見ファイル名ぽいキーを指定すればバケット内のS3画像オブジェクトは一意に決まるので、こんな感じの指定で取ってこれるようになります。 S3上のオブジェクトキーと同じ名前で画像ファイルを保存したければ、ダウンロード先を指定するresponse_targetlocal_path のように指定してあげればOKです。

# オブジェクトキーとダウンロード先を指定(後ほど解説)
object_key = 'huga.jpg'
local_path = "./#{object_key}" # huga.jpgという名前でカレントディレクトリにファイルがダウンロードされる

もうひとつ引っかかったのが、画像をアップロードするgemにpaperclipを使ってるときでした。

paperclipを使ってると、S3のオブジェクトキーが /paperclip/hoge/:hoge_id/hoge.jpeg のように、パスを含んだりします(設定によるのかな??)。

https://github.com/thoughtbot/paperclip

この場合は、オブジェクトキー指定時にはパスを含まなければなりません。スラッシュはただの文字みたいなものという認識で良さそう。

object_key = '/paperclip/hoge/:hoge_id/hoge.jpeg'
local_path = File.basename(object_key) #paperclipのフォルダパスを削除しファイル名に

Ruby(というかLinux)上はスラッシュがファイル名に入っているとよろしくないので、 local_path にはファイル名のみを指定しないと↓みたいなエラーが出てしまうので注意です。

Error getting object: No such file or directory @ rb_sysopen - ./paperclip/hoge/:hoge_id/hoge.jpeg

例外とリトライ

エラーはAws::S3::Errors::ServiceError で返ってくるので、rescueしたければそれを拾えば基本的にはOKです。

rescue Aws::S3::Errors::ServiceError => e
	# 一応aws-sdk-s3のエラーだってわかりやすい方が親切かも
	logger.error "error: #{e.message} in aws-sdk-s3"
end

https://docs.aws.amazon.com/sdk-for-ruby/v2/api/Aws/S3/Errors/ServiceError.html

あと、AWS SDK for Rubyはデフォで3回リトライしてくれるっぽいので、自分たちのコード部分で変にリトライ書かなくても良いかもです。認証に失敗すると、「コード内でのリトライ数 * AWS SDKでのリトライ数」のリクエストが行われそうなので。

https://docs.aws.amazon.com/ja_jp/sdk-for-ruby/v3/developer-guide/timeout-duration.html

Rspecとスタブ

テストするときは、クライアントをinitalizeする時にとりあえず stub_responcesをtrueにすれば良さそうです。

s3_client = if Rails.env.test?
	Aws::S3::Client.new(stub_responses: true)
	    else
	Aws::S3::Client.new(region: 'ap-northeast-1')
            end

https://docs.aws.amazon.com/ja_jp/sdk-for-ruby/v3/developer-guide/stubbing.html

しかしこれだと空のモックを作っただけで、設定値や戻り値・例外の検証ができず非常にザルな感じになってしまうので、 stub_responses の設定を細かく作ってあげるのが良さそうです。

個人的にスタブを設定するコードをRspec関連のファイル以外にあまり置きたくないので、別でsupportファイル作るのがベターかなと思います。

./spec/support/aws-sdk-s3.rb
# こんな感じで色々登録できる(はず)
Aws.config[:s3] = {
  stub_responses: {
    list_buckets: {
      buckets: [name: 'hoge']
    },
    list_objects: {
      contents: [{key: "hoge.jpg"}]
    },
  }
}

この辺は登録すべき振る舞いや返り値など、より実践的でベターなテストの記述方法があると思うので、知見をお持ちの方はぜひ教えて下さい。

備考

AWS SDKは、使う認証によってたくさんのCredentialクラスから最適なやつを選ばないといけないので(まあ当然っちゃ当然)、僕みたいに雑にしか理解できてないと少し苦労するかも。

Aws::Credentials
Aws::AssumeRoleWebIdentityCredentials
Aws::AssumeRoleCredentials
Aws::SharedCredentials
Aws::ProcessCredentials
Aws::InstanceProfileCredentials
Aws::ECSCredentials
Aws::CognitoIdentityCredentials

https://docs.aws.amazon.com/sdk-for-ruby/v3/api/index.html

↑のやつ色々試していて沼っていた側面もあったけど、「前提」のとおりEC2環境のIAM::Roleを使って認証していたので、コード上でCredentialsを設定する必要なかったのでした...。

お世話になった記事

AWS SDk for Rubyは公式ドキュメントが充実しており実装を進める上で大きな不安はないですが、上記の認証周りやテスト運用などを踏まえると、自チームと同じケースの利用をしている方が意外と多くない印象でした。たくさんググりましたので、参考になった記事を置いておきます(大変ありがとうございます)。

https://buildersbox.corp-sansan.com/entry/2020/04/13/110026

https://qiita.com/Y_uuu/items/4ce4cfdec1334cacaa49

https://qiita.com/Ushinji/items/4a845be4fb8f1c6fcc97

https://thr3a.hatenablog.com/entry/20190729/1564408054