📝

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

2022/06/07に公開

はじめに

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アカウントおよびリソースにアクセスする必要がないため、セキュリティ面での懸念点が少ない。

構成図


今回は、golangで実装されたgRPC serverからのupload処理およびdownload処理を実行する際に外部ストーレージとして機能するAWS S3をモッキングします。

GitHub Actionsで自動テストを実行するための設定

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

設定ファイル完成版

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コンテナを起動する設定

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の作成&ダミーファイルのアップロード

      - 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/

Discussion