DynamoDB localを使ってローカルで開発する

インフラ担当の柴田です。

さて、突然ですがDynamoDBを使った開発をどうやっていますか? DynamoDBがないと開発できないのですが、AWS上にDynamoDBを用意してローカルから接続する場合クレデンシャルの取り扱いが面倒くさいですよね。 かといって、毎回AWS環境にデプロイしないと確認できないのも面倒くさい。AWS Cloud9?いやほら、オフラインじゃ使えないじゃないですか……

ということで、今更なところはありますがDynamoDB Localを使ってローカル開発環境を構築してみました。

DynamoDB Local

docs.aws.amazon.com

DynamoDB Localはローカルで実行可能なDynamoDBで、JARファイルやDockerイメージが配布されています。 公式のDockerイメージがあるので、簡単に試して見ることができるのが良いですね。

Visual Stdio Code (VSCode)のRemote Containersと組み合わせる

VS CodeのRemote Containers便利ですよね。 私は最近簡単なコードはPythonで書くのですが、プロジェクト毎にコンテナ上に開発環境を作れるので、 Pythonの仮想環境機能を使わなくても環境やPythonのバージョンを分離できるのが楽で愛用しています。

そこで今回はRemote Containersに用意されているPythonの設定とDynamoDB Localのコンテナイメージを利用して環境を構築してみました。

作業用フォルダーをVSCodeで開いた後、少し手抜きをするためにまず、Remote ContainersのReopen in Containerを選びます。 コンテナ定義が無い状態でReopen in Containerを選択すると、「Add Development Container Configuration File」とコンテナ定義の追加メニューが表示されますので、 「Show All Definitions...」を選びテキストの入力エリアに「python」と入力して「Python3」を選び、使いたいバージョンを選択していくだけで簡単にPythonの開発環境が用意できます。

細かい利用方法や設定についてはVSCodeのドキュメント「Developing inside a Container using Visual Studio Code Remote Development」もあわせてご確認下さい。VSCodeのドキュメントでは「Open Folder in Container」ではじめていますが、コンテナで開くフォルダの選択の有無ぐらいの違いです。

Python3の定義ファイル選択

これだけではPythonは使えますが、まだDynamoDB Localが使えないのでDynamoDB Localの設定をしていきます。

先ほどのReopen in Containerによって.devcontainerというフォルダーが作成されるはずなので、.devcontainerフォルダーの中にdocker-compose.ymlというファイルを作成して以下の内容を入力します。 Pythonのコードを書く用のコンテナのappサービスと、dynamodb-localを動かすコンテナdynamodb-localサービスを定義しています。

appサービスのargsキーは同じく.devcontainerフォルダーにあるdevcontainer.jsonbuild.argsプロパティと同じ内容を入力します。また、AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYは任意の値で問題ないので、変更は不要です。

version: "3"
services:
  app:
    container_name: petstore
    build:
      context: .
      dockerfile: Dockerfile
      args:
        - VARIANT=3.9
        - INSTALL_NODE="false"
        - NODE_VERSION="lts/*"
    working_dir: /workspaces/petstore
    volumes:
      - ..:/workspaces/petstore:cached
    ports:
      - 8080:8000
    environment:
      AWS_ACCESS_KEY_ID: "DUMMYIDEXAMPLE"
      AWS_SECRET_ACCESS_KEY: "DUMMYEXAMPLEKEY"
      AWS_DEFAULT_REGION: "ap-northeast-1"
    tty: true
  dynamodb-local:
    command: "-jar DynamoDBLocal.jar -sharedDb -optimizeDbBeforeStartup -dbPath ./data"
    image: "amazon/dynamodb-local:latest"
    container_name: dynamodb-local
    ports:
      - "8000:8000"
    volumes:
      - "./docker/dynamodb:/home/dynamodblocal/data"
    working_dir: /home/dynamodblocal

次に、devcontainer.jsonを以下の様に書き換えます。VSCodeにインストールしている拡張機能や、「Add Development Container Configuration File」で選んだオプション等によって内容は変わってきます。 ポイントはbuildプロパティは削除してdockerComposeFileプロパティでdocker-compose.yml指定するのと、serviceプロパティを使ってdocer-compose.ymlで定義したサービス名を指定することで、VSCodeで接続するコンテナを指定します。

{
    "name": "Python 3",
    "dockerComposeFile": "docker-compose.yml",
    "service": "app",
    "workspaceFolder": "/workspaces/petstore",
    // Set *default* container specific settings.json values on container create.
    "settings": {
        "python.pythonPath": "/usr/local/bin/python",
        "python.linting.enabled": true,
        "python.linting.pylintEnabled": true,
        "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8",
        "python.formatting.blackPath": "/usr/local/py-utils/bin/black",
        "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
        "python.linting.banditPath": "/usr/local/py-utils/bin/bandit",
        "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8",
        "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy",
        "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle",
        "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle",
        "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint"
    },
    // Add the IDs of extensions you want installed when the container is created.
    "extensions": [
        "ms-python.python"
    ],
    // Use 'forwardPorts' to make a list of ports inside the container available locally.
    // "forwardPorts": [],
    // Use 'postCreateCommand' to run commands after the container is created.
    // "postCreateCommand": "pip3 install --user -r requirements.txt",
    // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
    //"remoteUser": "vscode"
}

入力が終わったら、Remote ContainersのRebuild Containerを選べば、コンテナが再ビルドされ、PythonのコンテナとDynamoDB Localのコンテナが立ち上がります。

これで、DynamoDB Localを使った開発環境が構築できました。

DynamoDB Localにテーブルを作ってみる

今回はPynamoDBというPythonのライブラリを利用します。PynamoDBについては改めて紹介できればと思いますが、DynamoDBをO/Rマッパー風に操作できるライブラリーです。

最初に、pipを使ってPynamoDBのパッケージを導入していきます。

pip3 install pynamodb pynamodb-attributes

つづいてDynamoDBに作るテーブルのモデルを作成します。作業フォルダーにdynamodb.pyというファイルを作成して以下の内容を入力します。

from unicodedata import category, name
from enum import Enum
from pynamodb.models import Model
from pynamodb.attributes import UnicodeAttribute, ListAttribute, MapAttribute
from pynamodb_attributes import UnicodeEnumAttribute, IntegerAttribute


class Status(str, Enum):
    available = "available"
    pending = "pending"
    sold = "sold"

class _Category(MapAttribute):
    id = IntegerAttribute()
    name = UnicodeAttribute()


class _Tag(MapAttribute):
    id = IntegerAttribute(hash_key=True)
    name = UnicodeAttribute()


class _Pet(Model):
    class Meta:
        table_name = "pet_table"

    id = IntegerAttribute(hash_key=True, null=True)
    category = _Category(null=True)
    name = UnicodeAttribute()
    photoUrls = ListAttribute(of=UnicodeAttribute)
    tags = ListAttribute(of=_Tag, null=True)
    status = UnicodeEnumAttribute(Status, null=True)


class PetRepo:
    def __init__(self) -> None:
        self.model = _Pet

その後、先ほど作ったモデルを呼び出して、テーブルを作成するコードを作成します。作業フォルダにcreate.pyというファイルを作って以下の内容を入力します。

from dynamodb import PetRepo
DDB_LOCAL_HOST = "http://dynamodb-local:8000"

if __name__ == "__main__":
    repo: PetRepo = PetRepo()

    model_meta_class = getattr(repo.model, "Meta")
    setattr(model_meta_class, "host", DDB_LOCAL_HOST)
    setattr(model_meta_class, "billing_mode", "PAY_PER_REQUEST")

    repo.model.create_table(wait=True)

DDB_LOCAL_HOST = "http://dynamodb-local:8000"がDynamoDB Localに接続するための設定で、docker-compose.ymlで定義したDynamoDB Localのサービス名とポートを指定します。

最後に、create.pyを実行すればDynamoDB Local上に、テーブルが作成されます。

なお、このコードはJX通信社エンジニアブログさんの記事で紹介されていた、手法を参考にしてみました。

tech.jxpress.net

テーブルの確認

さて、本当にテーブルができたのかを確認していきましょう。 今回はコンテナの中では無く、ホスト側(私の場合はMacになります)から確認してみようと思います。

私の場合はMacにAWS CLIが導入されてるのでそれを使います。最初にテスト用のprofileを作ります。 以下のコマンドで、localというプロファイルを作成します。Access Key IDSecret Access Keyは何でも大丈夫です。Default region nameap-northeast-1にします。これは、アプリケーションのコンテナを作ったときにap-northeast-1をデフォルトのリージョンに指定しているので、DynamoDBのテーブルがap-northeast-1に作成されているためです。

$ aws configure --profile local
AWS Access Key ID [None]: dummy
AWS Secret Access Key [None]: dummy
Default region name [None]: ap-northeast-1
Default output format [None]: json

profileの作成が完了しましたら、以下の様にdynamodb list-tablesを使うことで、テーブルの一覧が取得できます。--endpoint-urlでlocalhostを指定する事でDynamoDB Localへ接続しています。

$ aws --profile local  dynamodb list-tables --endpoint-url http://localhost:8000
{
    "TableNames": [
        "pet_table"
    ]
}

まとめ

駆け足になってしまいましたが、DynamoDB Localを利用した開発環境の雰囲気は伝わりましたでしょうか。

ローカルでDynamoDBを実行できるということは、AWSを使わずにテストができるようになるなど利点がありますので、 これからは積極的に使っていきたいと思います。