AppRunner上のLaravelアプリからDynamoに接続する

## 前提
- AppRuunerでLaravelのアプリを既にデプロイしている
- VPC ConnectorでVPCに接続している
- 追加要件としてDynamoを使う

## LaravelアプリでのDynamoへの接続設定
`baopham/laravel-dynamodb`を利用する。
Dynamoのデータ操作をEloquentライクでできるようになるライブラリ。

https://github.com/baopham/laravel-dynamodb
```bash:インストール方法
composer require baopham/dynamodb
php artisan vendor:publish --provider 'BaoPham\DynamoDb\DynamoDbServiceProvider'
# config/dynamodb.phpが作成されます。
```
詳細な使い方は`READ.ME`を参照。
Eloquentを継承しているので、基本的にはDynamoの検索方法（PK、SK、GSI）さえ押さえればEloquentモデルを使うように使えると思う。

### ローカル環境での接続
Dockerでローカル環境構築していることを前提とする。
ローカル用には以下のDynamoDB Localを使う。

https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/DynamoDBLocal.html

また、データ確認用のGUIツールも用意する。

https://github.com/aaronshaf/dynamodb-admin

それぞれを`compose.yml`に記載する。
```yml:compose.yml
dynamodb:
  image: amazon/dynamodb-local
  ports:
    - "8000:8000"
  volumes:
    - "./infra/docker/dynamodb/data:/data"
  command: ["-jar", "DynamoDBLocal.jar", "-sharedDb", "-dbPath", "/data"]
  
dynamodb-admin:
  image: aaronshaf/dynamodb-admin
  ports:
    - "8001:8001"
  environment:
    - DYNAMO_ENDPOINT=http://dynamodb:8000
  depends_on:
    - dynamodb
```

#### ローカル環境での認証設定
baopham/dynamodbを使う場合でも、Dynamoへの接続はAWSのSDKを使っている。
SDKを使ってAWSのリソースに接続する場合は認証情報の設定が必要になるが、ローカル環境の場合は環境変数に`DYNAMODB_CONNECTION=local`、`DYNAMODB_LOCA
L_ENDPOINT=http://dynamodb:8000`を設定すれば、上記で作成したローカルのコンテナに接続可能。

#### ローカル環境でのテーブル作成
DynamoDB Adminで直接作成してもいいが、今回はマイグレーションコマンドを作成する。実際のAWSで構築するときはCDKなどで定義したほうがいいと思う。
<details open><summary>サンプルコード</summary>

```php:DynamoMigrateCommand.php
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use BaoPham\DynamoDb\DynamoDbClientService;

final class DynamoMigrateCommand extends Command
{
    protected $signature = 'dynamo:migrate';
    protected $description = 'DynamoDBのテーブルを作成します。';

    public function handle()
    {
        $this->info('DynamoDBのテーブルを作成します。');

        if (!app()->environment('local')) {
            $this->error('このコマンドはローカル環境でのみ実行可能です。');
            return;
        }

        try {
            $this->createTable();
            $this->info('✅ テーブルが作成処理が完了しました。');
        } catch (\Exception $e) {
            $this->error('❌ テーブルの作成に失敗しました: ' . $e->getMessage());
        }
    }

    private function createTable()
    {
        $client = resolve(DynamoDbClientService::class)->getClient();

        $tableName = 'test-table-local';

        // 既にテーブルが存在するか確認
        $tables = $client->listTables();
        if (in_array($tableName, $tables['TableNames'])) {
            $this->warn("⚠️ テーブル '{$tableName}' はすでに存在します。スキップします。");
            return;
        }

        $params = [
            'TableName' => $tableName,
            'KeySchema' => [
                ['AttributeName' => 'PK', 'KeyType' => 'HASH'],
                ['AttributeName' => 'SK', 'KeyType' => 'RANGE'],
            ],
            'AttributeDefinitions' => [
                ['AttributeName' => 'PK', 'AttributeType' => 'S'],
                ['AttributeName' => 'SK', 'AttributeType' => 'S'],
            ],
            'BillingMode' => 'PAY_PER_REQUEST',
        ];

        $client->createTable($params);
    }
}
```
</details>

あとはModelを定義して、通常のEloquentを使うようにデータの操作が可能になる。

### AWS環境での接続
#### 1. 接続用の環境変数設定
アプリの環境変数に`DYNAMODB_CONNECTION=aws_iam_role`を設定する。
この設定で、アクセスキーやシークレットを利用せずに、AppRunnerのサービスロールを使ってDynamoに接続可能。

#### 2. AppRuunerのサービスロール作成
AppRunnerのサービス用のロールを作成する（ECR アクセスロールではなく、セキュリティ>インスタンスロールで設定するロール）。
インスタンスロールには、
- Dynamoへのアクセス許可ポリシー
- 以下の信頼ポリシー
を設定する。
```json:信頼ポリシー
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "tasks.apprunner.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
```
https://docs.aws.amazon.com/ja_jp/apprunner/latest/dg/security_iam_service-with-iam.html#:~:text=%E9%81%B8%E6%8A%9E%E3%81%A7%E3%81%8D%E3%81%BE%E3%81%99%E3%80%82-,%E3%82%A4%E3%83%B3%E3%82%B9%E3%82%BF%E3%83%B3%E3%82%B9%E3%83%AD%E3%83%BC%E3%83%AB,-%E3%82%A4%E3%83%B3%E3%82%B9%E3%82%BF%E3%83%B3%E3%82%B9%E3%83%AD%E3%83%BC%E3%83%AB%E3%81%AF

:::note warn
この設定をしないと以下のエラーが発生する。どうやらAWS SDKが見に行く認証用の設定が2種類あり、EC2用のものを参照してしまっているため起きている。以下参考。
https://blog.pinkumohikan.com/entry/could-not-retrieve-credential-when-using-aws-sdk-from-php-app-on-ecs

```
Error retrieving credentials from the instance profile metadata service. (cURL error 7:  (see https://curl.haxx.se/libcurl/c/libcurl-errors.html) for http://169.254.169.254/latest/meta-data/iam/security-credentials/)
```
:::

#### 3. VPCエンドポイントの設定
既存のVPCにNAT Gatewayが作成されている場合は2までの設定で接続可能。 
ただし、コスト面やセキュリティ等を考慮してVPCエンドポイント（Gateway型）を経由して通信させる。

作業としては、Gateway型のDynamo用VPCエンドポイントを作成し、対象のサブネットに割り当てる。
これで設定は完了。

このままだとNAT GatewayかVPCエンドポイントのどちらを経由しているか分からないので、CloudTrailでログを確認してみる。
- Dynamoのイベントログが設定されていない場合、新規で証跡を作成し、イベントログでDynamoのログを有効化
- CloudTrailコンソールの「イベント履歴」からは確認できない？ので、出力先のS3バケットにあるファイルを確認。`dynamodb`でgrepして、ヒットしたレコードの`vpcEndpointId`の値が設定したVPCエンドポイントのものになっていれば通信出来ている。
- 不要であれば作成した証跡は削除しておく。

## おわり
以上で通信は可能になったもののベストプラクティスかどうかは不明なので誰かの参考までに。