cdkでDynamoDB*Lambda*golangでタスク管理アプリケーションを作成してみる　概要とパーティション設計編（第一回）

さて、第五回のシリーズでざっくりcdkなどの理解はできたので、理解を深めるために超シンプルな「タスク管理アプリケーション」を作成したいと思います！

# タスク管理アプリケーションの機能
今回は以下のような機能を作成してみたいと思います。
- __タスクの作成、読み取り、更新、削除（CRUD）__
タスクを作成、読み取り、更新、削除する基本機能。
各タスクには、タイトル、説明、ステータス（未完了、進行中、完了）などのフィールドを持つ。

- __タスクのタグ付けとフィルタリング__
タスクにタグを付けることができ、タグでフィルタリング可能にする。

- __タスクの検索__
タスクをタイトルや説明で検索できるようにする。

画面のイメージはこんな感じで考えています。
![スクリーンショット 2024-06-01 15.39.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3324306/4458f927-ed01-79b5-0320-3fc5db60d0ef.png)


## 学べるポイント
- __Partition Key（PK）とSort Key（SK）__

- __DynamoDBのテーブルでPKとSKをどのように設定するか__

- __Global Secondary Index（GSI）__
タスクの検索やフィルタリングのためにGSIを設定。
例えば、タスクのステータスやタグに基づくGSIを設定することで、特定の条件でタスクを検索可能にする。
今回はGSIOverloadingでDynamodbらしいパターンを採用

- __CDKを使ったインフラストラクチャのコード化__
AWS CDKを使用して、DynamoDBテーブル、Lambda関数、API Gatewayなどをコードでデプロイ

- __LambdaとGoを使ったバックエンドロジック__
GoでLambda関数を作成し、DynamoDBと連携してタスクのCRUD操作を実装。
Lambdaを使用して、サーバーレスアーキテクチャの基本を学ぶ。

# 実装手順の概略
アプリケーションの実装は以下の流れで行おうと思います。
DynamoDBとCDKはそこまでボリュームはないと踏んでいるので第一回でまとめようと思います。

その後は状況を見ながらですが、Lambda関数をgolangで数回に分けて解説していこうと思います。
- __DynamoDBテーブルの作成__
PKとSKを設定したDynamoDBテーブルをCDKで作成。
GSIとLSIを設定
- __CDKプロジェクトのセットアップ__
CDKのプロジェクトを初期化し、必要なパッケージをインストール。
- __Lambda関数の実装__
GoでLambda関数を作成し、DynamoDBと連携するコードを記述。
タスクのCRUD操作を実装。
- __API Gatewayの設定__
API GatewayをCDKで設定し、Lambda関数にルーティング。
エンドポイントを作成し、タスク管理APIを公開。
- __フロントエンド（オプション）__
時間があれば、簡単なフロントエンドを作成して、ユーザーがタスクを管理できるUIを提供。

# パーティション設計
では、早速ですが今回のパーティションを設計していきましょう！
色々と記事を参考にさせてもらいながら考えてみます。(参考にさせてもらった記事は最後にまとめておきます。)

Dynamodbの設計で必要となる概念については以下で解説していますので参考にしてみてください。
https://qiita.com/YokoYokoko/items/8f0a19b0da69dc28f512

ですが、上記の内容を使用しながら再度説明をまとめていこうと思います。

## DynamoDBでは2種類のプライマリーキーをサポートする
- PartionKey
- PartionKey & Sort Key

![スクリーンショット 2024-05-31 22.26.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3324306/87830815-95ab-dabf-9ea7-f134f5d9caa4.png)

## LocalSencandaryIndexとGlobalSecondaryIndex
- __LocalSecondaryIndex__

これは第二、第三のSortKeyと考えると分かりやすいと思います。
SortKeyを属性Aに割り当てているが、属性BとPrimaryKeyの複合で検索する場合などに利用できるものです。

ただ、テーブル作成時にのみ定義可能なので後から追加や削除できません。

例えば、オンラインストアの注文管理システムで、注文テーブルがあり、プライマリキーが顧客ID（パーティションキー）と注文日時（ソートキー）で構成されているとします。

ここで、注文ステータスに基づいてデータをクエリしたい場合、注文ステータスをソートキーとするLSIを設定することで、特定の顧客の特定のステータスの注文を効率的に検索できるようになります。

- __GlobalSecondaryIndex__

これはPKとSKを完全に自由に設定できます。しかも、最大20個まで、後から追加や削除が可能なので要件が変化しても柔軟に対応することができます。

ただ、デメリットとしてストレージコストが追加でかかったり、ベーステーブルの書き込みにわずかな遅延があったり等が挙げられるので何でもかんでもGSIを使えばいいと良いという訳ではないようです。

## キャパシティユニット
DynamoDBにおける「キャパシティユニット」は、データベースの読み取りと書き込みの能力を測定するための単位です。これは、DynamoDBのプロビジョニングされたスループットモデルにおいて、リソースの使用量を管理し、コストを計算する基本的な要素です。
- __Read 4KB/s__
1ユニットにつき、最大4KBのデータを1秒に1回読み込み可能（強い一貫性を持たない読み込み設定の場合は1秒あたり2回）。なので、あまりデータの最新性を必要としない場合はイベンチュアル一貫性が良いですね。

- __Write 1KB/s__
1ユニットにつき、最大1KBのデータを1秒に1回書き込み可能

つまり、100WCUで設定した場合、1秒間に100KBを最大100KBのデータを書き込む能力があることを意味します。

例えば、アイテムのサイズが全て3KBの場合、1つのアイテムを書き込むのに3WCUが必要になります。そのため、100 WCUでは1秒間に約33件（100 /3）の3KBサイズのアイテムを書き込むことができます。

# DynamoDBのベストプラクティス
- __テーブルの数は最小限に留める__
一ヶ所にあるデータにテーブルやインデックスをつうじアクセスすることで望む形のデータを入手しやすいように構成

- __DynamoDBはPKまたはPK+SKでデータを識別子アクセスする__
ScanまたはQuery等のAPI利用。調べているとScanはやはり全件見ることとなり効率が悪いのであまり使用されない傾向がありそう。

- __GSIは元テーブルから非同期レプリケーションされる別テーブルのような存在__
先ほどのGSIの説明でも触れましたが、GSIを使うとストレージコストが余分にかかります。
その理由はGSIでは、ベーステーブルのデータの一部または全部を異なるプライマリキー構成で保存し、独自の方法でアクセス可能にする「別のテーブル」として機能するからです。

- __1テーブルや1インデックスに複数種類のアイテムを持たせても良いし、1属性に複数種類の値を入れても良い__
これはRDBに慣れているとかなり違和感がありますが、RDBみたいに正規化してテーブルを分割するということはあまりせずに、まとめられる情報は1テーブルにまとめます。

## 設計プロセス
### RDBとDynamoDBのモデリングの違い
- __RDBの設計プロセス__
![スクリーンショット 2024-05-31 23.01.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3324306/236bc41f-e706-aea7-9e44-f6f1472460c4.png)

RDBの場合だとエンティティを洗い出し、ER図を作成するところから始めるのが一般的かと思います。しかし、DynamoDBの場合は以下のようなプロセスで行います。

- __DynamoDBの設計プロセス__
![スクリーンショット 2024-05-31 23.01.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3324306/5b6c82b6-ff1a-5ab6-1607-dce750dc3a5c.png)

DynamoDBでもER図による概念整理やドメイン理解は有効のようです。

しかし、様々なところで言われていましたが、何よりも`アクセスパターンを整理してリスト化`することが重要なようです。この理由としては、これまで見てきたようにインデックスをどの属性に貼るかが、効率的な検索に最重要だからです。

DynamoDBのデータモデリングは以下の順序で行うと良いようです。また、1度設計したから完成というわけではなく、このプロセスのサイクルを回し続けることも大切です。

- アプリケーションを理解する
- ER図を作成
- `アクセスパターン`を全てリスト化
- テーブルやインデックスのスキーマを設計し
- クエリの詳細を設計、定義
流れがよくある流れのようです。

# タスク管理アプリケーションのパーティション設計
では、ここまで理解できればあとは実践あるのみです。
考えながら、分からないところを調べて追加で補足していくスタイルでやりましょう。

## モデリング&ER図作成
まずは、先ほどの流れに沿って、ドメインのデータをモデリングしER図で整理してみます。
![スクリーンショット 2024-05-31 23.14.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3324306/ab54507f-b718-6ac3-2061-d7feb3e9ed44.png)

今回の要件は以下の通りです。
```
- __タスクの作成、読み取り、更新、削除（CRUD）__
タスクを作成、読み取り、更新、削除する基本機能。
各タスクには、タイトル、説明、ステータス（未完了、進行中、完了）などの属性を持つ。

- __タスクのタグ付けとフィルタリング__
タスクにタグを付けることができ、タグでフィルタリング可能にする。

- __タスクの検索__
タスクをタイトルや説明で検索できるようにする。
```

まずは、この要件からエンティティを洗い出してテーブルにすると、以下のような案が一案だと思います。
```
- タスクテーブル
TaskID(PK)
title
description
status

- タグ
TagID(PK)
name

- タスクタグ（中間テーブル）
task_id(FK)
tag_id(FK)
```
これをER図で書くと、以下の通りです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3324306/f1b32f4c-b3de-6cf6-6939-f526149fda8b.png)

あるセッションではアクセスパターンをすべてER図にして不得意な領域を別サービスに切り分けたり、統合したりする方法も紹介されていました。これは別の機会に試そうと思います。今回はかなり簡略化したER図で行っていきます。

## アクセスパターン設計
ER図を書いてどういう要素が必要なのかある程度把握してまとめられたので、続いてはアクセスパターン設計を行います。
ここからがDynamoDBの設計っぽい感じがしますね。
![スクリーンショット 2024-05-31 23.14.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3324306/446bfcba-b51b-b1ea-11c6-7d7d03ce3936.png)

アクセスパターンの設計は以下の流れで行うと良さそうです。
1. アクセスパターンの洗い出し
テーブルにどのようなアクセスの仕方があるか、例えばtask_idでタスクを取得する・更新するなどであったり、tag_idとtask_idでタスクの状況を取得するなどであったり等です。

|Entity|Usecase|Input Parameters|Return|Description|
|:----:|:-----:|:--------------:|:----:|:---------:|
|タスク|getTaskById|{"TaskId"}|{"TaskId", "Title", "Status", "Description", "Tags"}|task_idでタスクを一件取得|
|タスク|getTasks|{}|[{"TaskId", "Title", "Status", "Description", "Tags"}]|タスク一覧取得*|
|タスク|getTasksByTaskIds|{["id1", "id2" ...]}|[{"TaskId", "Title", "Status", "Description", "Tags"}]|TaskIdsでタスク一覧取得|
|-タスク|getTasksByTag|{"Tag"}|[{"TaskId", "Title", "Status", "Description", "Tags"}]|タグでタスク一覧を取得|
|タスク|getTasksByTitle|{"Title"}|[{"TaskId", "Title", "Status", "Description", "Tags"}]|タイトルでタスク一覧を取得|
|タスク|getTasksByStatus|{"Status"}|[{"TaskId", "Title", "Status", "Description", "Tags"}]|ステータスでタスク一覧を取得|
|タスク|createTask|{"Title", "Description", "Tags"}|"StatusCode: http.StatusCreated", Body:"Task created successfully"|タスクを作成|
|タスク|deleteTaskById|{"TaskId"}|"StatusCode: http.StatusOK,Body:"Task deleted successfully",|task_idで指定されたタスクを削除|
|タグ|addTagToTask|{"TaskId", "Tag"}|"StatusCode: http.StatusOK,Body:"Tag added to task successfully"|タスクにタグを追加|
|タグ|updateTagOnTask|{"TaskId", "OldTag", "NewTag"}|"StatusCode: http.StatusOK,Body:"Tag updated successfully"|タスクに紐づくタグを更新|
|タグ|updateTitleOnTask|{"TaskId", "NewTitle"}|"StatusCode:http.StatusOK,Body:"Title updated successfully"|タスクに紐づくタイトルを更新|
|タグ|updateStatusOnTask|{"TaskId", "NewStatus"}|"StatusCode:http.StatusOK,Body:"Status updated successfully"|タスクに紐づくステータスを更新|
|タグ|updateDescriptionOnTask|{"TaskId",  "NewDescription"}|"StatusCode:http.StatusOK,Body:"Description updated successfully"|タスクに紐づく説明を更新|

* 暫定的なアクセスパターンのため修正の可能性大 2024/6/15

## テーブルやインデックスのスキーマを設計
 続いて、ユースケースをもとに何も考えずにテーブルに落とし込んでみると以下のようなテーブルとなります。

 ### 何も考えずにシンプルに設計
 
![スクリーンショット 2024-06-02 22.11.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3324306/ea2713b4-189c-f752-3f00-889c7312aeff.png)

ただ、これではいくつかの懸念点があります。
- GetItemやQueryがEventIdにしか使えず、それ以外の属性で検索したい場合 Scan + Filterで高コストとなってしまう。
色々調べていると、インデックスがない場合、項目数が少なくScanで問題ない場合が担保されている以外は、`Scan+Filter`は良くない検索方法として挙げられることが多い模様。
→ Scanではなく、QueryやGetItem / BatchGetItemの利用を検討するべき

次にGSIを取り入れて設計したパターン
![スクリーンショット 2024-06-02 22.21.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3324306/a39e65d5-80c5-30db-3c90-3c7f7d76d591.png)

### GSIを取り入れた設計
各属性にGSIを取り入れることにより、Queryが可能になリます。
しかし、これでも懸念点は残っており、GSIでは、別テーブルが作成されるためストレージコストが増加し、さらにスループットや管理コストが増加してしまう。

1つの案ではあるが、ベストプラクティスとは言えない。

### GSIOverloadingを取り入れた設計
解決策として今回はGSIOverloadingが有効な手段です。
GSIOverloadingでは、RDBのカラムだと以下のように横に並んでいる各カラムですが、
| 1 | 2 | 3 | 4 | 5 |
|:-:|:-:|:-:|:-:|:-:|
|TaskName|Title|Description|Status|Tag|

これを1つのカラムに縦に並べるイメージを持つと分かりやすいかもです。
| 1 |
|:-:|
|TaskName   |
|Title   |
|Description   |
|Status   |
|Tag   |

すると、以下のような形とすることが可能になります。
先ほど各属性にGSIを付与したときはGSI-PKは５つ、GSI-SKは1つ必要となり、非常に高コストでスループットにも問題を抱えそうな構成となっていましたが、このようにすることでGSI-PKは1つ、GSI-1-SKも1つと抑えることができました。
| PrimaryKey|Attributes |Attributes|
|:-:|:-|:-|
| PK, GSI-1-SK |SK|GSI-1-PK|
| TaskId | DataType | DataValue | 
|||
| {TaskId} | TaskName | {TaskName} |
| {TaskId} | Title | {Title} |
| {TaskId} | Description |{Description} |
| {TaskId} | Status | {Status} |
| {TaskId} | Tag | {TagName1, TagName2, ...} |

これでインデックスを使用し、scanせずに検索が可能となるパターンは以下の通りです。
1.**TaskId(PK)での検索**:タスク詳細・一覧表示
```
主キー（`PK`）が`TaskId`であるため、`GetItem`や`Query`操作を使用して、特定の`TaskId`を持つタスクのすべてのアイテムを取得します。
```
例えば、`TaskId=1`を指定して、取得することで、タスク名、タイトル、説明、ステータス、タグなど、そのタスクIDに関連付けられたすべてのアイテムを取得できる。

2.**TaskIdとDataTypeで(SK)の検索**:個別アイテムのタイトルやステータスの更新
```
`PK`が`TaskId`で、`SK`が`DataType`であるため、`Query`操作を使用して、特定の`TaskId`と`DataType`の組み合わせを持つアイテムを取得します。
```
例えば、`TaskId=1`とDataType=Titleを指定して、取得することで、そのタスクのタイトルのみを取得できる。

3.**DataValue(GSI-1-PK)とDataType(SK)での検索**:タグやタイトルで検索
```
`GSI-1-PK`が`DataValue`で、`SK`が`DataType`であるため、`Query`操作を使用して、特定の`DataValue`と`DataType`の組み合わせを持つアイテムを取得します。
```
例えば、DataTypeがStatusでDataValueが`complete`の場合、ステータスが"complete"であるすべてのタスクのアイテムを取得できる。

今回の要件はおそらく上記3つで満たせそうですが、副次的に以下の検索をインデックスを使用して可能となります。
4.**DataValue(GSI-1-PK)での検索**:
```
`GSI-1-PK`が`DataValue`であるため、`Query`操作を使用して、特定の`DataValue`を持つアイテムを取得できる。
```
例えば、`DataValue=complete`を指定して、取得することで、`complete`ステータスを持つアイテムを取得することができる。ただし、3で検索する方が効率的に検索を行うことが可能なので使用しない。

5.**DataValue(GSI-1-PK)とTaskID(GSI-1-SK)での検索**:
```
`GSI-1-PK`が`DataValue`で、GSI-1-SKが`TaskId`であるため`Query`操作を使用して、特定の`DataValue`と`TaskId`の組み合わせを持つアイテムを取得できる。
```
例えば、DataValueが特定のタグ名で、TaskIdが特定のタスクIDであれば、そのタグを持つ特定のタスクの情報を取得できる。

# クエリ条件定義
ここまでで、テーブル設計までできたので、ユースケースに合わせて次に具体的なクエリを検討します。
| # | Entity | UseCase | Parameters | Table/Index | API & Key Conditions |
|:-:|:-:|:-:|:-:|:-:|:-:|
|1|Tasks|createTask|{task}|Table|PutItem|
|2|Tasks|getTaskById|{taskId}|Table|GetItem(PK = :taskId)|
|3|Tasks|getTasks||Table|Query on PK|
|4|Tasks|updateTaskById|{taskId}|Table|UpdateItem|
|5|Tasks|deleteTaskById|{taskId}|Table|DeleteItem|
|6|Tasks|getTasksByTitle|{title}|GSI-1|Query(GSI-1-PK  = :title)|
|7|Tasks|getTasksByDescription|{description}|GSI-1|Query(GSI-1-PK  = :description)|
|8|Tasks|getTasksByStatus|{status}|GSI-1|Query(GSI-1-PK  = :status)|
|9|Tasks|getTasksByTag|{tagName}|GSI-1|Query(GSI-1-PK  = :tagName)|
|10|Tasks|addTagToTask|{taskId, newTag}|Table|UpdateItem - Add newTag to Tag list|
|11|Tasks|updateTagOnTask|{taskId, oldTag, newTag}|Table|UpdateItem - Replace oldTag with newTag in Tag list|
|12|Tasks|deleteTagFromTask|{taskId, tagToDelete}|Table|UpdateItem - Remove tag from Tag list|
* 要修正 2024/6/17
# まとめ
blackbeltの資料やテックブログを参考に簡単ではありますが、タスク管理アプリケーションのパーティション設計を行いました。
今後属性やEntityが増えた際の拡張性を持たせたりなどの改善の余地はありそうですが、とりあえずは一通り形になりました。

正規化を検討するRDBの設計とは異なるため、少し違和感がありますが何度か設計すれば慣れてきそうか気がします（願望）
ただ、やってみて感じたのは、blackbeltにも書かれている通り、以下の手順でまとめていくことが重要だということです。

特に個人的にはユースケースをまとめるところは、規模が大きければ大きいほど骨が折れる作業だと感じました。

![スクリーンショット 2024-05-31 23.01.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3324306/5b6c82b6-ff1a-5ab6-1607-dce750dc3a5c.png)

次回は、今回設計したdynamodbとlambda（golang）をcdkでデプロイしてみたいと思います！
かなり手探りなので何か補足等あればご指摘いただけると嬉しいです。

## 参考
https://speakerdeck.com/_kensh/dynamodb-design-practice
https://qiita.com/_kensh/items/2351096e6c3bf431ff6f