見出し画像

GitHub ActionsでLocalStackを動かしてみた

本記事は電通デジタルアドベントカレンダー2021 20日目の記事になります。

はじめに

AWSに依存した実装箇所をテストする際、皆さんはどのように行なっているでしょうか?多くの場合 AWS SDK などでMockを用意する、自前でStubを用意されているかと思います。そしてこのテストがGitHub Actionsで走ってくれると嬉しいですよね。

電通デジタル開発部の長内です。

今回はLocalStackとGitHub Actionsを使って極々単純なテストを書いてみます。

LocalStackとLocalStack Proの紹介

LocalStackのリポジトリには以下のように書かれています。

LocalStack 💻 is a cloud service emulator that runs in a single container on your laptop or in your CI environment.With LocalStack, you can run your AWS applications or Lambdas entirely on your local machine without connecting to a remote cloud provider!

https://github.com/localstack/localstack

用途としては文面通りローカルマシンやCI環境で活用可能であることが読み取れるかと思います。ただしいくつかの機能については有償のPro版のみで解放されており、使いたい機能が無償版で問題ないのかPro版でなければ使えないのかについては確認が必要です。詳しくは公式サイトをご参照ください。

LocalStackをGitHub Actionsで動かしてみる

挙動確認のための実行

まずはLocalStackが正常にGitHub Actionsで動くのか知りたいため、公式ドキュメントの中の LocalStack in CI / GitHub Actions のExampleを用いて動作確認してみます。さらにここではPro版の機能が使えるかどうか確認したいので DynamoDB のバックアップの実行も指定しています(本記事のPro版の出番はここまでです)。Pro版含むLocalStackの機能の対応状況については専用のページが用意されているので参照してみてください。なおPro版の動作のため今回はAPI Keyをリポジトリのsecretsに含んでいますが、その手順も前述のLocalStack in CI / GitHub Actions に含まれているので確認してみてください。

name: CI
on:
  pull_request:
    branches:
      - main
    paths:
      - 'sandbox/**'
jobs:
  backend-test:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: sandbox
    steps:
      - uses: actions/checkout@v2
        with:
          persist-credentials: false
      - uses: actions/setup-python@v2
        with:
          python-version: "3"
      - name: LocalStack Test
        env:
          LOCALSTACK_API_KEY: ${{ secrets.LOCALSTACK_API_KEY }}
        run: |
          pip install localstack awscli-local[ver1]
          docker pull localstack/localstack
          DOCKER_FLAGS='-d' localstack start
          echo "Waiting for LocalStack startup..."
          for i in {1..45}; do if [ `docker logs localstack_main | grep 'Ready.'` ]; then break; fi; sleep 1; done
          echo "Startup complete"
      - name: Run some Tests against LocalStack
        run: |
          awslocal s3 mb s3://sandbox
          awslocal s3 ls
          awslocal dynamodb create-table --table-name Sandbox --attribute-definitions AttributeName=Id,AttributeType=S AttributeName=Data,AttributeType=S --key-schema AttributeName=Id,KeyType=HASH AttributeName=Data,KeyType=RANGE --billing-mode PAY_PER_REQUEST
          awslocal dynamodb create-backup --table-name Sandbox --backup-name SandboxBackup
          echo "Test Execution complete!"

GitHub ActionsでLocalStack Proを起動すると、ローカルマシンでの動作と同様にPro版で動作していることを示すログが出力されます(以下のログのSuccessfully activated API key の箇所)。Pro版で動作させたい場合はこのログの出力有無を確認すると良いでしょう。なお当然ですがPro版の機能を使っているにも関わらずLocalStackのAPI Keyを設定していない場合はエラーが出力されます。

Status: Downloaded newer image for localstack/localstack:latest
docker.io/localstack/localstack:latest

     __                     _______ __             __
    / /   ____  _________ _/ / ___// /_____ ______/ /__
   / /   / __ \/ ___/ __ `/ /\__ \/ __/ __ `/ ___/ //_/
  / /___/ /_/ / /__/ /_/ / /___/ / /_/ /_/ / /__/ ,<
 /_____/\____/\___/\__,_/_//____/\__/\__,_/\___/_/|_|

 💻 LocalStack CLI 0.13.0

[08:58:10] starting LocalStack in Docker mode 🐳               localstack.py:106
──────────────── LocalStack Runtime Log (press CTRL-C to quit) ─────────────────
2021-11-17T08:58:12:INFO:localstack_ext.bootstrap.licensing: Successfully activated API key
['docker', 'run', '-e', 'LOCALSTACK_API_KEY=***', '-e', 'DEFAULT_REGION=us-east-1', '-e', 'LOCALSTACK_HOSTNAME=localhost', '-e', 'TEST_AWS_ACCOUNT_ID=000000000000', '-e', 'DOCKER_HOST=unix:///var/run/docker.sock', '-e', 'HOST_TMP_FOLDER=/tmp/localstack', '-d', '--rm', '--privileged', '--name', 'localstack_main', '-p', '127.0.0.1:4566:4566', '-p', '127.0.0.1:4571:4571', '-p', '127.0.0.1:12121:12121', '-v', '/tmp/localstack:/tmp/localstack', '-v', '/var/run/docker.sock:/var/run/docker.sock', 'localstack/localstack']
Waiting for LocalStack startup...
Startup complete

このGitHub Actionsワークフローが実行された結果は以下の通りです。LocalStack Proの機能であるDynamoDBのバックアップも正常に行われていますね。

awslocal s3 mb s3://sandbox
make_bucket: sandbox
2021-11-17 09:53:22 sandbox
{
    "TableDescription": {
        "AttributeDefinitions": [
            {
                "AttributeName": "Id",
                "AttributeType": "S"
            },
            {
                "AttributeName": "Data",
                "AttributeType": "S"
            }
        ],
        "TableName": "Sandbox",
        "KeySchema": [
            {
                "AttributeName": "Id",
                "KeyType": "HASH"
            },
            {
                "AttributeName": "Data",
                "KeyType": "RANGE"
            }
        ],
        "TableStatus": "ACTIVE",
        "CreationDateTime": 1637142805.25,
        "ProvisionedThroughput": {
            "LastIncreaseDateTime": 0.0,
            "LastDecreaseDateTime": 0.0,
            "NumberOfDecreasesToday": 0,
            "ReadCapacityUnits": 0,
            "WriteCapacityUnits": 0
        },
        "TableSizeBytes": 0,
        "ItemCount": 0,
        "TableArn": "arn:aws:dynamodb:us-east-1:000000000000:table/Sandbox",
        "TableId": "2257a574-e1e0-4293-b5dc-4a498f150a30",
        "BillingModeSummary": {
            "BillingMode": "PAY_PER_REQUEST",
            "LastUpdateToPayPerRequestDateTime": 1637142805.25
        }
    }
}
{
    "ConsumedCapacity": {
        "TableName": "Sandbox",
        "CapacityUnits": 1.0
    }
}
{
    "BackupDetails": {
        "BackupArn": "arn:aws:dynamodb:us-east-1:000000000000:table/Sandbox/backup/SandboxBackup",
        "BackupName": "SandboxBackup"
    }
}
DynamoDB Backup complete!
{
    "TableNames": [
        "Sandbox"
    ]
}
{
    "BackupSummaries": [
        {
            "TableName": "Sandbox",
            "BackupArn": "arn:aws:dynamodb:us-east-1:000000000000:table/Sandbox/backup/SandboxBackup",
            "BackupName": "SandboxBackup"
        }
    ]
}
Test Execution complete!

S3のBucket作成をテストしてみる

LocalStack ProがGitHub Actionsで動作することは確認できました。続いてS3のBucketを作成する関数に対して簡単なテストを行ってみます。

今回は以下のようなサンプル関数を用意しました。
ファイル名はsample.pyとして保存します。

import os
import boto3

def create_bucket(bucket_name):
    session = boto3.Session(
        aws_access_key_id=os.environ.get('AWS_ACCESS_KEY_ID'),
        aws_secret_access_key=os.environ.get('AWS_SECRET_ACCESS_KEY'),
    )
    s3_client = session.client('s3', endpoint_url=os.environ.get('S3_ENDPOINT'))
    result = s3_client.create_bucket(Bucket=bucket_name)

    return result

この関数に対するテストは正常なバケット名を渡した時と異常なバケット名を渡した時を想定してみます。当然テストシナリオとしては雑ですが、GitHub Actionsで動くかどうかを確認するためのテストなのでここではその雑さを一旦許容します。
こちらのファイル名はtest_sample.pyとして保存します。

import unittest
import botocore.exceptions
from sample import create_bucket

class TestS3Bucket(unittest.TestCase):
    def test_create_bucket(self):
        test_bucket_name = 'test-sample-bucket'
        result = create_bucket(test_bucket_name)
        http_status_code = result['ResponseMetadata']['HTTPStatusCode']
        location = result['Location']
        self.assertEqual(http_status_code, 200)
        self.assertEqual(location, '/' + test_bucket_name)

    def test_exception_create_bucket(self):
        test_bucket_name = 'test_sample_bucket'
        with self.assertRaises(botocore.exceptions.ClientError) as e:
            result = create_bucket(test_bucket_name)

if __name__ == '__main__':
    unittest.main()

なおこの時のファイル構成は以下のようになっています。
__init__.pyは空のままで構いません。

.
├── __init__.py
├── sample.py
└── test_sample.py

そしてこれらをGitHub Actionsで動かすためのworkflowファイルのJobは以下のようにします。

jobs:
  example-job:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
        with:
          persist-credentials: false
      - name: Start LocalStack
        env:
          SERVICES: s3
        run: |
          pip install localstack awscli-local[ver1]
          docker pull localstack/localstack
          DOCKER_FLAGS='-d' localstack start
          echo "Waiting for LocalStack startup..."
          for i in {1..45}; do if [ `docker logs localstack_main | grep 'Ready.'` ]; then break; fi; sleep 1; done
          echo "Startup complete"
      - name: Run Sandbox Test
        env:
          AWS_ACCESS_KEY_ID: test
          AWS_SECRET_ACCESS_KEY: test
          S3_ENDPOINT: http://localhost:4566
        run: |
          python -m unittest test_sample.py

実行結果は以下のようになりました。
無事成功していますね。

おわりに

LocalStackをGitHub Actionsで動作させ、テストに組み込めることが確認できました。用途としては例えば今回のようにS3へ直接バケットを作成ような実装をテストしたり、DynamoDBやAthenaなどからスタブデータを取得するケースなどに使えるかもしれません。ただLocalStackに引っ張られる形でGitHub ActionsのWorkflowが完了するまでの時間がかなりかかるようになってしまったため、実業務で採用するにはその点を許容できるのか等を議論する必要があると感じています。