iimon TECH BLOG

iimonエンジニアが得られた経験や知識を共有して世の中をイイモンにしていくためのブログです

ローカル環境でDynamoDBを体験してみる

こんにちは!

株式会社iimonでエンジニアをしている遠藤です。

NoSQLデータベースってどんなタイプのものがあって、それぞれどういう特徴があるのか、概念的なことは何となくわかってきたような気がする(?)けど、実際にNoSQLのデータベースを使用して開発するイメージ(DBとSQLを使わずにやりとりするイメージ)があまり沸かないな…と思ったので、
今回はDynamoDB Localでローカルにデータベースを立てて、Djangoでデータをやり取りする簡単なAPIを生成することで、少しでもイメージを掴んでみよう!という記事になります。

この記事でやることは以下になります。

  • 実際にローカルで動かせる環境を構築する
  • Djangoで、DBに対してデータを操作するための簡単なAPIを生成する

記述に間違っているところがあればご指摘いただけますと幸いです。

■前提

  • Docker Composeをインストール済みであること

■使用するツールについて

  • DynamoDB Local

    AWSが公開しているもので、ローカル環境で動作することができるDynamoDB互換のデータベースです。

    ただ、WebサービスのDynamoDBとはいくつか異なる点があるので注意が必要です。(ここでは詳細は省略します。) docs.aws.amazon.com

    ローカルで動かすため料金がかからないのと、今回はとりあえずローカルで簡単に試してみたいという気持ちなので、こちらを使用します。

    javaまたはDocker上で動作するので、今回はDockerで起動します。

  • dynamodb-admin

    DynamoDBを使用する際に、GUIでテーブルを管理することができるツールです。

    Django AdminはデフォルトでRDBとの連携をサポートしていますが、今回はRDBではないため、こちらを使用します。

■ローカル環境の構築手順

1. プロジェクト用の空のディレクトリを生成
mkdir django-dynamodb-sample
2. プロジェクトディレクトリに移動
cd django-dynamodb-sample
3. requirements.txtを作成

Djangoとboto3をインストールするため、requirements.txtファイルを作成します。
DjangoAPIを作成し、DynamoDBとのデータの操作にboto3を使用します。

echo "Django==5.0" >> requirements.txt
echo "boto3==1.34.153" >> requirements.txt
4. Dockerfileの生成
cat <<EOF > Dockerfile
FROM python:3.12

ENV PYTHONUNBUFFERED 1

RUN mkdir /src
WORKDIR /src
COPY requirements.txt /src/
RUN pip install -r requirements.txt
COPY . /src/
EOF

RUN pip install -r requirements.txt で、先ほどrequirements.txtに記述した必要なソフトフェアをインストールします。

5. docker-compose.ymlを作成

DjangoアプリケーションとDynamoDB Localとdynamodb-adminを同時に実行するため、各サービスと、サービスが利用するDockerイメージを記述します。

cat <<EOF > docker-compose.yml
services:
  api:
    container_name: "api"
    build: .
    depends_on:
      - dynamodb-local
    volumes:
      - .:/src
    working_dir: /src
    command: python3 manage.py runserver 0.0.0.0:8000
    ports:
      - "9000:8000"
    
  dynamodb-local:
    container_name: dynamodb-local
    command: "-jar DynamoDBLocal.jar -sharedDb -dbPath ./data"
    image: "amazon/dynamodb-local:latest"
    ports:
      - 8000:8000
    volumes:
      - "./docker/dynamodb:/home/dynamodblocal/data"
    working_dir: /home/dynamodblocal
    
  dynamodb-admin:
    container_name: dynamodb-admin
    image: aaronshaf/dynamodb-admin:latest
    environment:
      - DYNAMO_ENDPOINT=dynamodb-local:8000
    ports:
      - 8001:8001
    depends_on:
      - dynamodb-local
EOF
6. Djangoプロジェクトの作成

Composeに対し、コンテナ内においてdjango-admin startproject src を実行します。

docker-compose run api django-admin startproject src .
7. DynamoDBLocalの設定をsrc/settings.pyに追加

今回はローカルで動かして触りたいだけなので、諸々環境変数に入れずにベタ書きしています。
DynamoDBにアクセスするため、boto3でリソースを取得します。

import boto3
    
DYNAMODB_LOCAL = True
    
if DYNAMODB_LOCAL:
  DYNAMODB_ENDPOINT_URL = 'http://dynamodb-local:8000'
else:
  DYNAMODB_ENDPOINT_URL = None
    
session = boto3.Session(
    aws_access_key_id='dummyAccessKey',
    aws_secret_access_key='dummySecretKey',
    region_name='ap-northeast-1'
)
    
dynamodb = session.resource(
    'dynamodb',
    endpoint_url=DYNAMODB_ENDPOINT_URL
)
8. Dockerを起動
docker-compose up -d --build
9. アプリケーションの作成

apiコンテナに入る

docker-compose exec api bash

apiコンテナ内で以下のコマンドを実行します。appはアプリケーション名となります。

python manage.py startapp app

src/settings.pyのINSTALLED_APPSに作成したアプリケーションを追加します

INSTALLED_APPS = [
    'django.contrib.admin',
    ...
    'app',  # 追加
]

http://localhost:8001/にアクセスして以下のような画面になっていればOKです。

http://localhost:9000/adminにもアクセスできるかと思います。
(ただ、今回はDjango Adminは使用しないので、後ほどエンドポイントを削除します。)

■実際に触ってみる

ローカルで触れる環境が整いました! 実際に簡単なAPIを構築してデータを操作してみます。

1. テーブルの作成

今回はコマンドを実行して、注文を管理するためのテーブルを作成できるようにします。

フォルダの作成

mkdir -p app/management/commands

ファイルを作成し、コマンドを追加

cat <<EOF > app/management/commands/create_order_tables.py
from django.core.management.base import BaseCommand
from src.settings import dynamodb
    
class Command(BaseCommand):
  help = 'Create Order tables'
    
  def handle(self, *args, **options):
    table_name = 'Order'
    try:
      existing_tables = dynamodb.meta.client.list_tables().get('TableNames', [])
      if table_name in existing_tables:
        self.stdout.write(self.style.SUCCESS(f'Table {table_name} already exists.'))
        return
    
      table = dynamodb.create_table(
        TableName=table_name,
        KeySchema=[
          {'AttributeName': 'id', 'KeyType': 'HASH'},
        ],
        AttributeDefinitions=[
          {'AttributeName': 'id', 'AttributeType': 'S'},
        ],
        ProvisionedThroughput={
          'ReadCapacityUnits': 3,
          'WriteCapacityUnits': 3
         }
      )
    
      table.meta.client.get_waiter('table_exists').wait(TableName=table_name)
      self.stdout.write(self.style.SUCCESS('Successfully created table'))
    except Exception as e:
      self.stdout.write(self.style.ERROR(f'Error creating table: {e}'))
EOF

apiコンテナ内でコマンドを実行

python manage.py create_order_tables

http://localhost:8001/にアクセスするとテーブルが追加されていることを確認できるかと思います。

コマンドの内容について少し触れます

  • KeySchema:
    テーブルのプライマリキーを構成する属性、またはインデックスのキー属性を指定します。

    ソートキーを設定しない場合は、パーティションキーがプライマリキー、
    ソートキーを設定する場合は、 パーティションキーとソートキーを複合したものがプライマリキーになります。
    今回はidパーティションキーに指定しており、ソートキーは指定していないため、idがプライマリキーになります。

    DynamoDBでは、アイテムごとに異なる属性を追加することができますが、パーティションキーやソートキーに設定している属性は必ず含めなければいけません。

  • AttributeDefinitions:
    KeySchemaプロパティの属性を定義する必要があります。 属性名と属性の型を指定します。

  • ProvisionedThroughput:
    料金形態の設定になります。

    • オンデマンドキャパシティモード:
      テーブルに対して実際のリクエスト数に対して料金が発生する。
    • プロビジョンキャパシティモード:
      テーブルに対して必要なスループットをキャパシティユニットとしてあらかじめ割り当てておく。割り当てた分の料金を支払う。

      • ReadCapacityUnits: 読み込みに対するキャパシティ
      • WriteCapacityUnits: 書き込みに対するキャパシティ

    どちらの形態を利用するかはBillingModeで指定します。デフォルトはプロビジョンキャパシティモードです。
    DynamoDB Localで実行される場合にはこちらの値は使用されません。

2. ビューの定義

app/view.pyを以下のコードに置き換えます。
今回はローカル環境を構築して、実際に触ってみたいだけなので、csrf_exemptデコレーターをインポートして、CSRFチェックを無効にしています。

DjangoのORMは使えないので、boto3で取得したリソースを使用して、データの操作を行います。

from django.http import JsonResponse, HttpResponse
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from src.settings import dynamodb
import json

@method_decorator(csrf_exempt, name='dispatch')
class OrderList(View):
  # テーブルの取得
  table = dynamodb.Table('Order')

  def get(self, request):
    try:
      response = self.table.scan()
      items = response.get('Items', [])
      return JsonResponse(items, safe=False)
    except Exception:
      return HttpResponse(status=400)

  def post(self, request):
    try:
      data = json.loads(request.body)
      self.table.put_item(Item=data)
      return JsonResponse(data, status=201)
    except Exception:
      return HttpResponse(status=400)
    
@method_decorator(csrf_exempt, name='dispatch')
class OrderDetail(View):
  # テーブルの取得
  table = dynamodb.Table('Order')
  def get(self, request, id):
    try:
      # Keyにはプライマリキーを指定
      response = self.table.get_item(Key={'id': id})
      item = response.get('Item')
      if item:
        return JsonResponse(item)
      return HttpResponse(status=404)
    except Exception:
      return HttpResponse(status=400)
    
  def delete(self, request, id):
    try:
      # Keyにはプライマリキーを指定
        self.table.delete_item(Key={'id': id})
        return HttpResponse(status=204)
    except Exception:
      return HttpResponse(status=400)
3. URLの設定
  • app/urls.py

ファイルを生成

touch app/urls.py

ファイルに追記

from django.urls import path
from .views import OrderList, OrderDetail
        
urlpatterns = [
  path('order-list/', OrderList.as_view(), name='order-list'),
  path('order-detail/<str:id>/', OrderDetail.as_view(), name='order-detail'),
]
  • src/urls.py

以下に書き換える

from django.urls import path, include
        
urlpatterns = [
  path('app/', include('app.urls')),
]
4. 動作確認
  • 注文の登録

一覧の全取得と指定したキーの値の取得を比較するために、2つデータを入れておきます。

curl -X POST http://localhost:9000/app/order-list/ -H "Content-Type: application/json" -d '{"id": "1", "item": "coffee", "quantity": 3}' 
curl -X POST http://localhost:9000/app/order-list/ -H "Content-Type: application/json" -d '{"id": "2", "item": "greenTea", "quantity": 2}' 
  • 注文一覧の取得
curl -X GET http://localhost:9000/app/order-list/

正しく動いていれば、先ほど登録したデータが全て取得できます。

[
  {
     "item": "coffee",
     "id": "1", 
     "quantity": "3", 
    }, 
    {
      "item": "greenTea",
      "id": "2",
      "quantity": "2",
    },
]%        
  • 指定した注文の取得

id1の注文を取得します。

 curl -X GET http://localhost:9000/app/order-detail/1/

以下のようなレスポンスが返ってくるかと思います。

{"item": "coffee", "id": "1", "quantity": "3"}%   
  • 指定した注文の削除

id1の注文を削除します

curl -X DELETE http://localhost:9000/app/order-detail/1/

http://localhost:8001/tables/Orderを見るとid1の注文が削除されています。

■まとめ

とりあえず簡単なcrud処理を実装することができました。

頭の中がリレーショナルリレーショナルしていたので、DjangoのORM使わずにどうやって操作するんだろう?とかなど色々思っていたのですが、DynamoDBはAWSのサービスだからboto3使えばいいのかーと腑に落ちました。 少しは実装のイメージが湧いたような気がしています。

今回はとりあえず触ってデータをやり取りするイメージを掴んでみようという回だったので、次回はもう少し複雑なデータの扱いを試したり、DynamoDB自体についてももっと詳しく調べてみたいと思います!

最後まで読んでくださりありがとうございます!

この記事を読んで興味を持って下さった方がいらっしゃればカジュアルにお話させていただきたく、是非ご応募をお願いいたします!!

Wantedly/Green

■参考文献

docs.aws.amazon.com

docs.aws.amazon.com

boto3.amazonaws.com

docs.docker.jp

zenn.dev

www.youtube.com