DynamoDB  Localトラブルシューティング（Node.js + TypeScript）

`Node.js + TypeScript（Dockerコンテナ）`から`DynamoDB Local`へ接続、操作をする際に発生したトラブルの備忘録です。

## DynamoDB Localとは？

AWS上のDynamoDBにアクセスすることなく、DynamoDBを利用するアプリケーションの開発・テストをすることが可能になります。

[DynamoDB Localの設定（ダウンロード版）](https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/DynamoDBLocal.html)

## 背景

前提として`Node.js + TypeScript`のプログラムはECS on Fargateのタスクスケジューラで定期実行するプログラムです。

今回DynamoDB Localを導入するのは、開発時にAWSのDynamoDBを利用せず、ローカル環境で全て完結させたいというのが理由です。

## 忙しい人のために

トラブル対応後のファイル構成、操作手順が下記になります。

[まとめ📖 （ファイル一覧/操作手順）](#まとめ)

## 事前準備

本実装に入る前にDynamoDB LocalをDockerで立ち上げて、AWS公式の**[Node.jsとDynamoDB](https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/GettingStarted.NodeJs.03.html)**のチュートリアルを参考に実装を進めることに。

### ファイル構成

```shell
$ tree
.
├── Dockerfile
├── package.json
├── src
│   └── index.ts
└── tsconfig.json
```

### DynamoDB Local立ち上げ

最初はdocker-composeを利用せず、下記コマンドでDynamoDB Localを立ち上げていました。（なるべく本番環境に不要なファイルを作りたくなかった）

```shell
$ docker run --name dynamodb -p 8000:8000 amazon/dynamodb-local
```

## トラブル（発生順）

### その1 : ~ is not assignable to parameter of type 'ConfigurationOptions & ConfigurationServicePlaceholders & APIVersions'

```javascript
import * as AWS from 'aws-sdk';

AWS.config.update({
  region: 'us-west-2',
  endpoint: 'http://localhost:8000'
});

const dynamodb = new AWS.DynamoDB.DocumentClient();
```

```shell
TSError: ⨯ Unable to compile TypeScript:
src/index.ts(14,3): error TS2345: Argument of type '{ region: string; endpoint: string; accessKeyId: string; secretAccessKey: string; }' is not assignable to parameter of type 'ConfigurationOptions & ConfigurationServicePlaceholders & APIVersions'.
  Object literal may only specify known properties, and 'endpoint' does not exist in type 'ConfigurationOptions & ConfigurationServicePlaceholders & APIVersions'.
```

#### 解決法

TypeScriptで実装しているため、型定義によるエラーが発生。詳細は省きますが、下記のようにプログラムを書き換えることで解消されました。

**修正ポイント ： `ServiceConfigurationOptions`を指定する**

```javascript
import * as AWS from 'aws-sdk';
import { ServiceConfigurationOptions } from 'aws-sdk/lib/service';

const serviceConfigOptions: ServiceConfigurationOptions = {
  region: 'us-west-2',
  endpoint: 'http://localhost:8000'
};
AWS.config.update(serviceConfigOptions);

const dynamodb = new AWS.DynamoDB.DocumentClient();
```

### その2 : Missing credentials in config

```javascript
import * as AWS from 'aws-sdk';
import { ServiceConfigurationOptions } from 'aws-sdk/lib/service';

const serviceConfigOptions: ServiceConfigurationOptions = {
  region: 'us-west-2',
  endpoint: 'http://localhost:8000'
};
AWS.config.update(serviceConfigOptions);

const dynamodb = new AWS.DynamoDB.DocumentClient();
```

```shell
Error Error: connect ECONNREFUSED 169.254.169.254:80
    at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1141:16) {
  message: 'Missing credentials in config, if using AWS_CONFIG_FILE, set AWS_SDK_LOAD_CONFIG=1',
  errno: 'ECONNREFUSED',
  code: 'CredentialsError',
  syscall: 'connect',
  address: '169.254.169.254',
  port: 80,
  time: 2020-08-23T23:30:29.548Z,
  originalError: {
    message: 'Could not load credentials from any providers',
    errno: 'ECONNREFUSED',
    code: 'CredentialsError',
    syscall: 'connect',
    address: '169.254.169.254',
    port: 80,
    time: 2020-08-23T23:30:29.547Z,
    originalError: {
      message: 'EC2 Metadata roleName request returned error',
      errno: 'ECONNREFUSED',
      code: 'ECONNREFUSED',
      syscall: 'connect',
      address: '169.254.169.254',
      port: 80,
      time: 2020-08-23T23:30:29.547Z,
      originalError: [Object]
    }
  }
}
```

AWS公式の**[Node.jsとDynamoDB](https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/GettingStarted.NodeJs.03.html)**のチュートリアルをTypeScriptに置き換えただけですが、動作せず・・・。

#### 解決法

設定値に**Access Key**、**Secret Access Key**がないとエラーになるようです。

**修正ポイント ： キーに`accessKeyId`、`secretAccessKey`を追加、値は任意の値でOK**

```javascript
import * as AWS from 'aws-sdk';
import { ServiceConfigurationOptions } from 'aws-sdk/lib/service';

const serviceConfigOptions: ServiceConfigurationOptions = {
  region: 'us-west-2',
  endpoint: 'http://localhost:8000',
  accessKeyId: 'fakeAccessKeyId',
  secretAccessKey: 'fakeSecretAccessKey'
};
AWS.config.update(serviceConfigOptions);

const dynamodb = new AWS.DynamoDB.DocumentClient();
```

### その3 : connect ECONNREFUSED 127.0.0.1:8000

```javascript
import * as AWS from 'aws-sdk';
import { ServiceConfigurationOptions } from 'aws-sdk/lib/service';

const serviceConfigOptions: ServiceConfigurationOptions = {
  region: 'us-west-2',
  endpoint: 'http://localhost:8000',
  accessKeyId: 'fakeAccessKeyId',
  secretAccessKey: 'fakeSecretAccessKey'
};
AWS.config.update(serviceConfigOptions);

const dynamodb = new AWS.DynamoDB.DocumentClient();
```

```shell
Error Error: connect ECONNREFUSED 127.0.0.1:8000
    at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1141:16) {
  errno: 'ECONNREFUSED',
  code: 'NetworkingError',
  syscall: 'connect',
  address: '127.0.0.1',
  port: 8000,
  region: 'us-west-2',
  hostname: 'localhost',
  retryable: true,
  time: 2020-08-24T23:03:41.246Z
}
```

localhostではDockerホストにアクセスできずエラーとなります（そりゃそうだ）

#### 解決法

解決方法としては**コンテナ間の通信を可能とする**、もしくは**Dockerホスト（Mac）を経由する**2通りが考えられますが、今回は後者で解決できました。

[Docker for Mac上のコンテナから、Mac上のアプリケーションに簡単に接続する方法](https://qiita.com/Asayu123/items/ccfe4ccfc417ce57f445)

**修正ポイント ： `endpoint`の値を http://docker.for.mac.localhost:8000 に変更**

```javascript
import * as AWS from 'aws-sdk';
import { ServiceConfigurationOptions } from 'aws-sdk/lib/service';

const serviceConfigOptions: ServiceConfigurationOptions = {
  region: 'us-west-2',
  endpoint: 'http://docker.for.mac.localhost:8000',
  accessKeyId: 'fakeAccessKeyId',
  secretAccessKey: 'fakeSecretAccessKey'
};
AWS.config.update(serviceConfigOptions);

const dynamodb = new AWS.DynamoDB.DocumentClient();
```

### その4 : ResourceNotFoundException: Cannot do operations on a non-existent table

DynamoDB Localへの接続で上手くいったので、テストデータを追加してプログラムを作成していきます。

追加は下記にアクセスして操作します。

http://localhost:8000/shell/

操作方法：[DynamoDBローカルをDockerコンテナとして動かす](https://qiita.com/tamo_breaker/items/7a2344032bc7e736b071)

ブラウザ上で追加したデータをNode.js + TypeScript（Dockerコンテナ）から取得してみましたが掲題のエラーに・・・。

テーブル一覧を取得しても、ブラウザ上で追加したテーブルがない状態・・・。

🤔 🤔 🤔 🤔 🤔

#### 解決法

DynamoDB Localのデータファイルが設定されている**アクセスキー（Access Key）**と**リージョン（Region）**によって決まってくるようです。

```
[Access Key]_[Region].db
```

[DynamoDB LocalでgetItemしたら、ResourceNotFoundException: Cannot do operations on a non-existent table が返ってきた](https://www.ninton.co.jp/archives/2562)

要するに**アクセスキー**と**リージョン**の値をDynamoDB Localの設定値とプログラムで指定する値を揃える必要があります。

DynamoDB Localのアクセスキーはブラウザ上のオプションから変更が可能です。（おそらくDynamoDB Localを立ち上げる際に環境変数で指定することも可能？）

![dynamodb-local.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116168/a2d24912-7794-efa5-9205-2a181bf3d35b.jpeg)

ちゃんと書いてありますね。 **the DB file is based upon the Access Key ID**


プログラム上でアクセスキーを`fakeAccessKeyId`と指定しているので、同じ値をブラウザ上で設定すると今度は上手くいきました。

ただ、今のままではDynamoDB Localを立ち上げるたびにAccess Keyを指定する必要があるので、データの永続化も含め**docker-compose**で対応します。

ルートディレクトリに`dynamodblocal`というDynamoDB Local用のディレクトリを新規に作成し、その中に**docker-compose.ymlとdataディレクトリを追加**します。

```shell
$ tree
.
├── Dockerfile
├── dynamodblocal
│   ├── data/
│   └── docker-compose.yml
├── package.json
├── src
│   └── index.ts
└── tsconfig.json
```

※ `./dynamodblocal/data/`ディレクトリはDynamoDB Localを起動する前に作成しておく

```docker:dynamodblocal/docker-compose.yml
version: "3"

services:
  dynamodb:
    image: amazon/dynamodb-local
    volumes:
      - ./data:/home/dynamodblocal/data
    ports:
      - "8000:8000"
    command: -jar DynamoDBLocal.jar -dbPath ./data -sharedDb
```

**<font color="Orange">DynamoDB Localを起動時にsharedDBオプションを指定することで、アクセスキーやリージョンに関係なくアクセスすることが可能になります。</font>**

## まとめ

```shell
$ tree
.
├── Dockerfile
├── dynamodblocal
│   ├── data
│   │   └── shared-local-instance.db
│   └── docker-compose.yml
├── package.json
├── src
│   └── index.ts
└── tsconfig.json
```

※ `./data/shared-local-instance.db`はコンテナ起動後、自動的に生成される（dataディレクトリだけ用意しておけばおｋ）

```docker:dynamodblocal/docker-compose.yml
version: "3"

services:
  dynamodb:
    image: amazon/dynamodb-local
    volumes:
      - ./data:/home/dynamodblocal/data
    ports:
      - "8000:8000"
    command: -jar DynamoDBLocal.jar -dbPath ./data -sharedDb
```

```typescript:src/index.ts
import * as AWS from 'aws-sdk';
import { ServiceConfigurationOptions } from 'aws-sdk/lib/service';

let serviceConfigOptions: ServiceConfigurationOptions = {
  region: 'us-west-2',
  endpoint: 'http://docker.for.mac.localhost:8000',
  accessKeyId: 'fakeMyKeyId',
  secretAccessKey: 'fakeSecretAccessKey'
};
AWS.config.update(serviceConfigOptions);

const dynamodb = new AWS.DynamoDB.DocumentClient();
```

### 起動

```shell
$ cd dynamodblocal/
$ docker-compose up -d
```

### 停止

```shell
$ docker-compose down
```