AWS SDK S3 for Rubyを用いてAPIを叩き画像オブジェクトをダウンロードする
※画像は『AWS SDK AWS Black Belt Tech Webinar 2015』のスライドから引用
微妙に沼ったので戒めを込めて書きます。zenn初カキコ...ども...
認証周りでたくさんアクセスディナーイされたのでとてもツラかった。
やりたいこと
- S3バケット上に保存されている画像オブジェクトを、API経由でダウンロードする
- 画像オブジェクトの指定には、S3の画像オブジェクトキー(getObjectKey)を指定する
- AWS公式が用意しているSDK(aws-sdk)を使ってダウンロードを行う
動作環境
- Ruby: 2.6.6
- Rails: 5.2.3
- aws-sdk-s3: 1.83.0
- aws-sdk-core : 3.109.1
前提
- Instance Profileを用いて、コードを動かしたいEC2に
s3:GetObject
の権限を付与したIAM::Role
を設定- なので、Rubyコード上で
access key id
とかsecret access key
を管理して設定する必要がなかった - 裏を返すと、コード上にIAM::Roleを渡しているコンテキストが残りづらいので、細かくコメント書いたほうが良いように思う
- なので、Rubyコード上で
コード
- 以下のコードを実行すると、
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
一見ファイル名ぽいキーを指定すればバケット内のS3画像オブジェクトは一意に決まるので、こんな感じの指定で取ってこれるようになります。 S3上のオブジェクトキーと同じ名前で画像ファイルを保存したければ、ダウンロード先を指定するresponse_target
をlocal_path
のように指定してあげればOKです。
# オブジェクトキーとダウンロード先を指定(後ほど解説)
object_key = 'huga.jpg'
local_path = "./#{object_key}" # huga.jpgという名前でカレントディレクトリにファイルがダウンロードされる
もうひとつ引っかかったのが、画像をアップロードするgemにpaperclip
を使ってるときでした。
paperclipを使ってると、S3のオブジェクトキーが /paperclip/hoge/:hoge_id/hoge.jpeg
のように、パスを含んだりします(設定によるのかな??)。
この場合は、オブジェクトキー指定時にはパスを含まなければなりません。スラッシュはただの文字みたいなものという認識で良さそう。
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
あと、AWS SDK for Rubyはデフォで3回リトライしてくれるっぽいので、自分たちのコード部分で変にリトライ書かなくても良いかもです。認証に失敗すると、「コード内でのリトライ数 * AWS SDKでのリトライ数」のリクエストが行われそうなので。
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
しかしこれだと空のモックを作っただけで、設定値や戻り値・例外の検証ができず非常にザルな感じになってしまうので、 stub_responses
の設定を細かく作ってあげるのが良さそうです。
個人的にスタブを設定するコードをRspec関連のファイル以外にあまり置きたくないので、別でsupportファイル作るのがベターかなと思います。
# こんな感じで色々登録できる(はず)
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
↑のやつ色々試していて沼っていた側面もあったけど、「前提」のとおりEC2環境のIAM::Role
を使って認証していたので、コード上でCredentialsを設定する必要なかったのでした...。
お世話になった記事
AWS SDk for Rubyは公式ドキュメントが充実しており実装を進める上で大きな不安はないですが、上記の認証周りやテスト運用などを踏まえると、自チームと同じケースの利用をしている方が意外と多くない印象でした。たくさんググりましたので、参考になった記事を置いておきます(大変ありがとうございます)。
Discussion