adobe/s3mockでS3をモック化する

# はじめに
`adobe/s3mock`を使うことでAWS S3をモック化することができます。
ローカル開発環境のDockerに`adobe/s3mock`を追加することで、実際のAWS S3を利用せずともS3の機能を実現することができます。

# 導入方法
## adobe/s3mockの追加
`docker-compose.yml`に下記を追加することで`adobe/s3mock`を利用することができます。

```diff:docker-compose.yml
services:
  web:
    ...
+  s3:
+    image: adobe/s3mock
+    environment:
+      - initialBuckets=my-bucket
+      - retainFilesOnExit=true
+      - root=/s3mock
+    ports:
+      - "9090:9090"
+    volumes:
+      - s3_data:/s3mock
  volumes:
+  s3_data:
      ...
```
`environment`に`initialBuckets`を指定することで、最初からバケットを作成しておくことができます。

`retainFilesOnExit`を`true`に指定し、`root`で指定したパスをvolume化することで、
投稿した画像を永続化することができます。

## aws-sdk-s3を使用した画像投稿
`adobe/s3mock`を導入できたので、早速`aws-sdk-s3`を使用して画像を投稿したいと思います。
まず`Gemfile`に`aws-sdk-s3`を追加します。

```:Gemfile
gem 'aws-sdk-s3', '~> 1'
```

`config/initializers/aws.rb`などのファイルを作成して、`region`, `credentials`を設定します。
ローカル環境で`adobe/s3mock`を使用する場合は、`endpoint`を`'http://s3:9090'`（`s3`は`docker-compose.yml`に記載したサービス名）、`force_path_style`を`true`に指定します。
AWS上のS3に実際アクセスはしないので、`ENV["AWS_ACCESS_KEY"]`と`ENV["AWS_SECRET_ACCESS_KEY"]`は適当な値で大丈夫です。

```ruby:config/initializers/aws.rb
Aws.config.update({
  region: 'ap-northeast-1',
  credentials: Aws::Credentials.new(ENV["AWS_ACCESS_KEY"], ENV["AWS_SECRET_ACCESS_KEY"])
})

if Rails.env.development?
  Aws.config.update({
    endpoint: 'http://s3:9090',
    force_path_style: true
  })
end
```

### 動作確認
`initialBuckets`に`my-bucket`を指定しているので、デフォルトで`my-bucket`のバケットが作成されていることが確認できます。
```ruby
s3 = Aws::S3::Resource.new
s3.buckets.each {|b| p b.name }
=> "my-bucket"
```

画像をアップロードすることもできます。
```ruby
file_path = Rails.root.join('sample.jpg')
obj = s3.bucket('my-bucket').object('image/sample.jpg')
obj.upload_file(file_path)
```

署名付URLを発行しアップロードした画像を表示することもできます。
ただし、`endpoint`に`http://s3:9090`を指定しているため、デフォルトでは
`http://s3:9090/my-bucket/image/sample.jpg?`から始まるURLが発行され
そのままではブラウザから画像を表示することができません。

なので、画像を表示する際には`endpoint`を`'http://localhost:9090'`に指定するといいと思います。


```ruby
client = Aws::S3::Client.new(endpoint: 'http://localhost:9090')
signer = Aws::S3::Presigner.new(client: client)
@url = signer.presigned_url(:get_object, bucket: 'my-bucket', key: 'image/sample.jpg')
=> "http://localhost:9090/my-bucket/image/sample.jpg?から始まるURL"
```

生成した`@url`は`image_tag`で実際に表示することができます。
```erb:index.html.erb
<%= image_tag @url %>
```

![スクリーンショット 2023-06-04 15.38.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2736970/75c56ef3-d932-cb7b-537f-3f0733e751bd.png)

## CarrierWaveを使用した画像投稿
`Gemfile`に`carrierwave`と`fog-aws`を追加します。
```:Gemfile
gem 'carrierwave'
gem 'fog-aws'
```

ローカル環境で`adobe/s3mock`を使用する場合は、`endpoint`を`'http://s3:9090'`、`force_path_style`を`true`に指定します。
ブラウザから画像を表示することができるよう
`config.asset_host`には`'http://localhost:9090/my-bucket'`を指定します。

```ruby:config/initializers/carrierwave.rb
require 'carrierwave/storage/abstract'
require 'carrierwave/storage/file'
require 'carrierwave/storage/fog'

CarrierWave.configure do |config|
  if Rails.env.development?
    config.storage :fog
    config.fog_provider = 'fog/aws'
    config.fog_directory  = 'my-bucket'
    config.fog_credentials = {
      provider: 'AWS',
      aws_access_key_id: ENV['AWS_ACCESS_KEY'], 
      aws_secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'], 
      region: 'ap-northeast-1',   
      endpoint: 'http://s3:9090',
      path_style: true
    }
    config.asset_host = 'http://localhost:9090/my-bucket'
  end
end
```

後は、画像を投稿したいモデルに「アップロード画像用のカラム」と「アップローダークラス」を用意し、
アップローダークラスの`storage`を`:fog`にすることで`adobe/s3mock`を使用した画像投稿が行えます。
```ruby
storage :fog
```

![スクリーンショット 2023-06-04 15.44.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2736970/adeab41c-ac42-77eb-4aad-18d224bd34f4.png)

![スクリーンショット 2023-06-04 15.45.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2736970/40c72a46-ea83-d1be-144c-76cbb706f661.png)


## ActiveStorageを使用した画像投稿
S3を利用するActiveStorageの設定にすれば大丈夫です。
`'aws-sdk-s3'`を利用するので、`endpoint`などの設定は`config/initializers/aws.rb`でしているので追加で設定を行う必要はありません。
```ruby:config_environments/development.rb
config.active_storage.service = :amazon
```

```yml:config/storage.yml
amazon:
  service: S3
  access_key_id: <%= ENV["AWS_ACCESS_KEY"] %>
  secret_access_key: <%= ENV["AWS_SECRET_ACCESS_KEY"] %>
  region: ap-northeast-1
  bucket: my-bucket
```

### 注意点
ActiveStorageを使用する場合、`endpoint`が`'http://s3:9090'`のままなので
このままだとブラウザから画像を表示することができません。
なので、ブラウザで画像も表示したい場合はさらにもう一工夫してあげる必要がありそうです。