AWS S3のmockを作成してGitHub Actionsで自動テストを実行する

# はじめに
CRUDなWebアプリケーションを開発する際に、外部ストレージとして`AWS S3`を選定し、ファイルのアップロード処理やダウンロード処理などを実装する場合が多々あると思います。
`AWS S3`のようなマネージドサービスに依存するビジネスロジックに関しても単体テストもしくは統合テストをきちんと書き、自動テストを実行したい！！　といった場合に役立ちそうな内容を記事にまとめます。

# 選定したツール
今回は`localstack`という、AWSのマネージドサービスをモッキングするためのツールを使用します。
`localstack`を使用することで、AWSの多くのマネージドサービスをモッキングすることが可能となります。
ツールを利用する際に一部有料となってしまう部分も存在しますが、今回モッキングを行いたいS3に関しては利用に際して無料となっているので、特に問題なく開発が行えそうです。

https://github.com/localstack/localstack

## 選定理由
`localstack`を選定するに至るまでいくつかの方法でS3をモックしてのテスト実行方法を模索してみましたがテスト環境の実装を進める中で不都合を感じたので断念しました。

### interfaceを定義してmockする
AWSの公式ドキュメント`AWS SDK for Go V2`にinterfaceを定義し、外部からDIすることでS3をmockする方法が紹介されていました。

https://aws.github.io/aws-sdk-go-v2/docs/unit-testing/

s3のmock方法を検証し始めた当初、こちらの方針で実装を進めていましたが、`テストコードを実装するためにプロダクションコードを複雑化させてしまっている`という点もあり、採用を断念しました。

### AWSステージングアカウントのテスト用bucketを使用する
こちらは、AWSのステージング用アカウントにテスト用のS3bucketを作成し、テスト実行のたびに該当bucketに対してアップロード処理やダウンロード処理を実行するという方法です。
こちらの方法については、`テストを実行するたびに、S3bucketのclean upができない`、`AWSのリソースにアクセスするためのIAMユーザの作成&管理を行う必要があるため運用の手間が増える`という点から採用を断念しました。

S3をmockする際の大替案となる上記の選択肢の比較検討を踏まえて、以下の理由から最終的に`localstack`を選定しました。

* `localstack`のdocker imageが公開されており、比較的容易にテスト環境の構築が行える。
* テスト実行のたびにテスト環境を一から構築するため、S3bucketのclean upが可能。
* S3のmock化のためにプロダクションコードを複雑化させるという懸念点が比較的少ない。
* 実際のAWSアカウントおよびリソースにアクセスする必要がないため、セキュリティ面での懸念点が少ない。

# 構成図

![](https://storage.googleapis.com/zenn-user-upload/8db55b51455b-20220607.png)
今回は、`golang`で実装された`gRPC server`からのupload処理およびdownload処理を実行する際に外部ストーレージとして機能するAWS S3をモッキングします。

# GitHub Actionsで自動テストを実行するための設定
`GitHub Actions`で自動テストを実行するための設定ファイルを作成していきます。
## 設定ファイル完成版

```yaml:grpc-test.yml
name: gRPC Test

on:
  push:
    paths:
      - "src/**"

defaults:
  run:
    shell: bash
    working-directory: src/backend-go

env:
  AWS_REGION: ap-northeast-1
  AWS_END_POINT_URL: http://localhost:4566
  AWS_S3_BUCKET: test-bucket

jobs:
  test:
    name: gRPC Test
    runs-on: ubuntu-latest
    services:
      mysql:
        image: mysql:5.7
        env:
          MYSQL_ROOT_PASSWORD: pass
          MYSQL_DATABASE: test
        ports:
          - 3307:3306
        options: >-
          --health-cmd "mysqladmin ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

      localstack:
        image: localstack/localstack
        env:
          SERVICES: s3
          DEFAULT_REGION: ${{ env.AWS_REGION }}
          DATA_DIR: /tmp/localstack/data
        ports:
          - 4566:4566

    steps:
      - uses: actions/checkout@v3

      - name: Set up Go
        uses: actions/setup-go@v3
        with:
          go-version: "1.18"

      - name: Install dependences
        run: go mod tidy

      - name: Install Protoc
        uses: arduino/setup-protoc@v1
        with:
          version: "3.19.4"

      - name: Start LocalStack
        run: |
          pip install awscli-local[ver1]
          awslocal s3 mb s3://${{ env.AWS_S3_BUCKET }} --endpoint-url=http://localhost:4566
          awslocal s3 cp ./testdata/dummyFile.xls s3://${{ env.AWS_S3_BUCKET }}/ --endpoint-url=http://localhost:4566

      - name: Make generate
        run: |
          go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
          go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2
          export PATH="$PATH:$(go env GOPATH)/bin"
          mkdir genproto
          make generate

      - name: Install migrate package
        run: |
          curl -L https://packagecloud.io/golang-migrate/migrate/gpgkey | sudo apt-key add -
          echo "deb https://packagecloud.io/golang-migrate/migrate/ubuntu/ $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/migrate.list
          sudo apt-get update
          sudo apt-get install -y migrate

      - name: Test database setup
        run: |
          mysql -h 127.0.0.1 --port 3307 -u root -D test -ppass < init.sql
          migrate -path ./migrations -database $DATABASE_URL up
        env:
          DATABASE_URL: mysql://root:pass@tcp(localhost:3307)/test

      - name: Run tests
        run: make test
        env:
          AWS_S3_BUCKET: ${{ env.AWS_S3_BUCKET }}
          AWS_ACCESS_KEY: localstack
          AWS_SECRET_KEY: localstack
          AWS_REGION: ${{ env.AWS_REGION }}
          AWS_END_POINT_URL: ${{ env.AWS_END_POINT_URL }}
```

## `localstack`コンテナを起動する設定

```yaml
jobs:
  test:
    name: gRPC Test
    runs-on: ubuntu-latest
    services:
    :
    :
      localstack:
        image: localstack/localstack
        env:
          SERVICES: s3
          DEFAULT_REGION: ${{ env.AWS_REGION }}
          DATA_DIR: /tmp/localstack/data
        ports:
          - 4566:4566
```
`job`定義の中のserviceの部分に`localstack`コンテナを起動するための設定を追記します。

## ダミーbucketの作成&ダミーファイルのアップロード
```yaml
      - name: Start LocalStack
        run: |
          pip install awscli-local[ver1]
          awslocal s3 mb s3://${{ env.AWS_S3_BUCKET }} --endpoint-url=http://localhost:4566
          awslocal s3 cp ./testdata/dummyFile.xls s3://${{ env.AWS_S3_BUCKET }}/ --endpoint-url=http://localhost:4566
```
`awslocal`コマンドを使用するために、`pip`のinstall処理で`awscli-local[ver1]`をインストールしましょう。
仮に実際のAWSリソースにアクセスする際と同様に`aws`コマンドを使用したとしても、`--endpoint-url`でlocal環境のエンドポイントを指定すれば、実際のAWSリソースにアクセスすることはないと思いますが、テスト環境では`awslocal`コマンドを使用してlocal環境のテスト用コンテナに対してコマンドが実行されるようにした方が安全なのかなと思います。
## 設定完了
上記の様に設定ファイルを作成しGitHubにpushすれば、AWS S3をmockした上で`GitHub Actions`を用いて自動テストを実行することができるようになっていると思います。

# 最後に
AWSのマネージドサービスに依存するテストを実行するとなると、テスト環境構築の際に幾つかの手間が発生してしまいますが、今回のように`localstack`を用いることで容易にテスト環境を構築することが可能となりました。
S3以外のその他のマネージドサービスに依存するビジネスロジックの実装をおこなった際には、今回の様に`localstack`の使用を検討したいと思います。

# 参考記事
https://qiita.com/ushio_s/items/1df34a53f8f948448bc5

https://docs.localstack.cloud/integrations/