LocalStack で Rails による S3 の投稿をモックする

LocalStack は、AWS のサービスを開発環境において擬似的に使用できるモックフレームワークです。
定番の S3 から Lambda まで多くのサービスを開発環境で体験できます。

本記事では、Railsチュートリアルの SampleApp を使用し、コンテナ環境で実装します。
コンテナの設定は、[Railsチュートリアルの開発環境を Docker でもっと便利にしなイカ！？ \- Qiita](https://qiita.com/Moo_Moo_Farm/items/8e0f92b9f45e08285d97)の続きとなります。

今回の実装コードは右の通りです＝＞([実装コード --github](https://github.com/masayuki-0319/sample_app/pull/8))

---

- 実行環境
  - Ruby: 2.4.9
  - Rails: 5.1.2
  - fog: 1.14.0
  - carrierwave: 1.1.0
- references
  - [localstack/localstack: 💻 A fully functional local AWS cloud stack\. Develop and test your cloud & Serverless apps offline\!](https://github.com/localstack/localstack)
  - [fog/fog: The Ruby cloud services library\.](https://github.com/fog/fog)
  - [carrierwaveuploader/carrierwave: Classier solution for file uploads for Rails, Sinatra and other Ruby web frameworks](https://github.com/carrierwaveuploader/carrierwave)

---

## □ LocalStack の設定
### ■ コンテナの追加

- version 0.11.0 から`http://localhost:4566`で全ての endpoint を受け付けている。
- `SERVICES`により、モックするサービスを定義する。
- `DATA_DIR: /tmp/localstack/data`により、投稿画像を永続化する。
- `volumes`で設定したディレクトリにスクリプトを配置すると、コンテナ生成時に実行してくれる( 詳細は後述 )。
- なお、access key 等を設定する必要はない。

```diff:./docker-compose.yml
  ...
  smtp:
    ...
+ localstack:
+   image: localstack/localstack
+   ports:
+     - 8080:8080 # dashboard
+     - 4566:4566 # edge port
+   environment:
+     SERVICES: s3
+     AWS_DEFAULT_REGION: ap-northeast-1
+     DATA_DIR: /tmp/localstack/data
+   volumes:
+     - ./docker/localstack/:/docker-entrypoint-initaws.d
  volumes:
    ...
```

### ■ 初期設定スクリプト

S3 保存用のバケットを生成する必要があるため、次のファイルを作成すること。

```sh:./docker/localstack/setup_s3.sh
awslocal s3 mb s3://microposts
```

## □ 既存ファイルの修正
### ■ 保存場所を S3 に固定

```diff:app/uploaders/picture_uploader.rb
class PictureUploader < CarrierWave::Uploader::Base
  ...
- if Rails.env.production?
-   storage :fog
- else
-   storage :file
- end
+ storage :fog
  ...
end
```

### ■ carrier_wave の初期設定を変更

- 変更内容
  - 本番環境でのみ設定が有効化されていたため、常に有効化した。
  - LocalStack にアップロードするための設定として、本番環境以外における設定を追加した。
- 参考 URL
  - [PHPからS3にファイルアップロードをlocalstackを使って開発してみた \- Qiita](https://qiita.com/nao_tuboyaki/items/86c9fcc7edb1bfb73f4b)
  - [LocalStackをDockerコンテナ内で使う場合に、S3のバケット名がサブドメインになって接続できなくて困った話（解決済み） \- Qiita](https://qiita.com/imunew/items/91f01a78a19a29caf3b1)
  - [localstackでS3をモックした \- 勉強日記](https://wand-ta.hatenablog.com/entry/2019/03/10/215639)

```ruby:config/initializers/carrier_wave.rb
CarrierWave.configure do |config|
  config.fog_credentials = {
    :provider              => 'AWS',
    :region                => ENV['S3_REGION'],
    :aws_access_key_id     => ENV['S3_ACCESS_KEY'],
    :aws_secret_access_key => ENV['S3_SECRET_KEY'],
  }
  config.fog_directory     =  ENV['S3_BUCKET']

  unless Rails.env.production?
    config.fog_credentials.merge!(
      {
        # [app -> localstack] コンテナ間の通信用に設定 ( http://localstack:4566 )
        :endpoint          => ENV['S3_ENDPOINT'],

        # デフォルトだと S3_BUCKET がサブドメインとなり接続できないため true に設定
        :path_style        => true,
      }
    )

    # endpoint がコンテナ間の通信用であるため、ホスト側から画像にアクセスするための URL ( http://localhost:4566 )
    config.asset_host = "#{ENV['S3_ASSET_HOST']}/#{ENV['S3_BUCKET']}"
  end
end
```

### ■ 環境変数の設定

```diff:./docker-compose.yml
  ...
  app:
    ...
    environment:
      APP_DATABASE_HOST: db
      APP_DATABASE_USERNAME: root
      APP_DATABASE_PASSWORD: pass
+     S3_REGION: ap-northeast-1
+     S3_ACCESS_KEY: dummy
+     S3_SECRET_KEY: dummy
+     S3_BUCKET: microposts
+     S3_ENDPOINT: http://localstack:4566
+     S3_ASSET_HOST: http://localhost:4566
    ...
```

## □ テスト投稿

|投稿前|
|---|
|<img width="1386" alt="スクリーンショット 2020-05-26 22.44.38.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/355926/b80bc2bf-6864-e736-900f-e77b9b776c4a.png">|

|投稿成功！|
|---|
|<img width="1387" alt="スクリーンショット 2020-05-26 22.46.07.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/355926/dda4e1ed-15b3-c49c-b5d5-e198299554bf.png">|

## □ 余談: 投稿画像の永続化

前述の通り`DATA_DIR`を追加すると、投稿画像の永続化が可能となるが、コンテナ停止再起動でのみ有効となる。
もし、コンテナ削除時でもデータを保持したい場合、次の通り設定すること。

また、この設定の場合、意識的に volume を削除しないと保存容量が膨れ上がるため、注意する必要あり。

```diff:./docker-compose.yml
services:
  datastore:
    image: busybox
    volumes:
      - bundle_install:/usr/local/bundle
      - db_data:/var/lib/postgresql/data
+     - localstack_data:/tmp/localstack
...
  localstack:
    image: localstack/localstack
    ports:
      - 8080:8080 # dashboard
      - 4572:4572 # s3
    environment:
      SERVICES: s3
      AWS_DEFAULT_REGION: ap-northeast-1
      DATA_DIR: /tmp/localstack/data
    volumes:
      - ./docker/localstack/:/docker-entrypoint-initaws.d
+     - localstack_data:/tmp/localstack
volumes:
  bundle_install:
  db_data:
+ localstack_data:
```

## □ メモ

### コンテナから S3 保存を確認

```ash
docker-compose exec localstack ash

aws --endpoint-url=http://localstack:4572 s3 ls s3://${バケット名}
```

### S3 に保存したファイルをコンテナ内にダウンロード

```ash
docker-compose exec localstack ash

aws --endpoint-url=http://localstack:4572 s3 cp #{key} ~/
cat ~/#{key}
```
### ブラウザからファイルを確認

```
open http://localhost:4572/#{バケット名}

# TODO: ファイルの中身は 403 によりアクセス不可，何かしら設定が必要なはず
open http://localhost:4572/#{key}
```