CakePHP4でDynamoDBを参照する

## はじめに

DynamoDBを利用する際に、CakePHPとDynamoDBの接続に関する記事はバージョンが古かったり、作成するファイルが多かったりして、DB接続に手こずったため備忘録として今回記事にしました。

## この記事でやること
CakePHP4を用いてDynamoDBに接続し、テーブル情報を取得する。

## 前提
- EC2を利用しています
    - OSイメージ:Amazon Linux 2 Kernel 5.10
- CakePHPのバージョン: 4.2.12
- nginx: nginx/1.22.0
- PHPのバージョン: PHP 7.4.33 (cli)
- PHP-FPM: PHP 7.4.33 (fpm-fcgi)
- AWS SDK for PHPのバージョン: 3.257

## AWS側での準備

### DynamoDBでテーブル作成

AWSコンソールのホーム＞DynamoDB＞テーブル＞テーブルの作成から適当なテーブルを作成します。

### AWS SDKのインストール

AWSを操作するときはWeb上からコンソールを使って操作していましたが、プログラムからAWS上にある各種サービスを利用するにはAWS SDKが必要です。

AWS SDKとはAWSのサービスをプログラムから操作するための開発キット(開発に必要なAPIやライブラリをまとめたもの)のことで、開発言語別に提供されています。

今回はAWS SDK for PHPを使います。
インストールにはcomposerを使います。([composerのインストール](https://getcomposer.org/download/))

```
cd プロジェクトのベースディレクトリ
composer require aws/aws-sdk-php
```

/vendor内にawsディレクトリが作成されていればOKです


### IAMロールの作成

コンソールからログインする際にパスワードを入力する必要があるように、プログラムからAWS上のリソースにアクセスする際にも認証情報が必要です。

認証情報としてAWSアクセスキーを取得します。

アクセスキーを取得する方法はユーザータイプや認証方法によって3通りあります。（参考: [AWS アクセスキーの取得](https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/SettingUp.DynamoWebService.html))

IAMユーザーでAWSへアクセスする場合はそのうち2つの方法があります。
- IAMユーザーから直接アクセスキーを取得する(長期間の認証情報)←AWS非推奨
- IAMロールを作成し、EC2インスタンスに紐付ける。アクセスする度に一時的な認証情報を自動的に取得する(短期間の認証情報)

今回はIAMロールを利用し、短期間の認証情報を使用してAWSへリクエストを送る方式を採用しました。

DynamoDBへアクセスできるポリシーを持ったIAMロールを作成します。

- IAM＞ロール＞ロールの作成
- 信頼されたエンティティタイプ
    - AWSのサービス
- ユースケース
    - EC2
- 許可ポリシー
    - AmazonDynamoDBFullAccess
- ロール名
    - role_test

作成したIAMロールをEC2インスタンスに紐付けます。

- EC2＞インスタンスを選択＞アクション＞セキュリティ＞IAMロールを変更
- role_testを選択し「IAMロールの更新」

## PHP側での準備

### クライアントオブジェクトをインスタンス化する

DynamoDB上のデータベースを操作するためにはクライアントオブジェクト(今回はDynamoDBオブジェクト)をインスタンス化する必要があります。

クライアントオブジェクトにはサービスのAPIを実行するためのメソッドが入っています。

インスタンス化にはコンストラクタ(クラス生成時に実行されるメソッド)へ設定オプションを連想配列で引数として渡す必要があります。

下の例のようにクライアントオブジェクトを直接インスタンス化することもできますが、今回はSdkクラスをインスタンス化→DynamoDBをインスタンス化という流れにしています。

例.
```php
//Create an S3Client
$s3 = new Aws\S3\S3Client([
    'version' => 'latest',
    'region' => 'us-east-2'
]);
```

Sdkクラスにオプションの連想配列を渡すことで、複数クライアント共通の設定オプションとして使うことができます。

今回はDynamoDBだけを使用しましたが、S3など他のAWSサービスを同時に使いたいときには、いちいち設定オプションを書かなくてすむので便利そうです。

オプションの連想配列には`region`と`version`の値しか設定していませんが、クライアントオブジェクトが生成される時に、先程作成したIAMロールに紐付けたEC2インスタンスが一時認証情報を取得するため、この時アクセスキーも参照されています。

今回はplugin以下にファイルを作成して記述しています。

#### plugins/aws/Client/aws.php

```php
<?php
namespace aws\Client;

use Aws;
use Aws\DynamoDb;
use Cake\Core\Configure;

/**
 * AWS共通クラス
 */
class AwsClient
{
    private $_config = null;
    private $_sdk = null;
    private $_dynamodb = null;

    /**
     * コンストラクタ
     */
    public function __construct()
    {
        $this->_sdk = new Aws\Sdk([
            'region' => 'ap-northeast-1',
            'version' => 'latest',
        ]);
    }

    /**
     * DynamoDB のクライアント取得
     *
     * @return $this->dynamodb DynamoDbClient
     */
    public function getDynamoDbClient()
    {
        if ($this->_dynamodb === null) {
            $this->_dynamodb = $this->_sdk->createDynamoDb();
        }

        return $this->_dynamodb;
    }
}
```

#### plugins/aws/DynamoDb.php


``` php
<?php

namespace aws;

use aws\Client\AwsClient;
use Utill\StrictComparison;

/**
 * DynamoDBインターフェイス
 */
class DynamoDb
{
    private $_client = null;
    /**
     * Constructor
     *
     * @param \Aws\Client\AwsClient $awsClient AwsClient Object
     */
    public function __construct(AwsClient $awsClient = null)
    {
        if (StrictComparison::isNull($awsClient)) {
            $awsClient = new AwsClient();
        }
        $this->_client = $awsClient->getDynamoDbClient();
    }

    /**
     * キー名からレコードを取得する
     * 
     * @param string $tableName テーブル名
     * @param array $key プライマリーキー
     * @return object 一致するレコード
    */
    public function getItemByKey(string $tableName, array $key)
    {
        return $this->_client->getItem([
            'Key' => $key,
            'TableName' => $tableName,
        ]);
    }
}

```



### テーブル情報を取得する

DynamoDBの接続に必要な処理が書けたので最後にControllerでDynamoDBクラスをインスタンス化して指定したキーの値からレコードを取得します。

#### src/Controller/DynamoDbController.php

``` php
<?php
declare(strict_types=1);

namespace App\Controller;

use Cake\Controller\Controller;
use aws\DynamoDb;

class DynamoDbController extends Controller
{
    public $tableName = '作成したテーブル名';
    public $key = [
        'キー名' => [
            'S' => '検索したい値'
        ],
    ];

    public function index()
    {
        $dynamoDb = new DynamoDb();
        $result = $dynamoDb->getItemByKey($this->tableName, $this->key);
        // $resultの値をtemplateに渡すことでview側で取得した内容を使用できるようになります。
        $this->set(compact('result'));
    }
}

```

var_dump等で結果を確認すると対象のレコードが取得できていることを確認できるはずです。

## まとめ

今回の手順を使えばDynamoDBだけでなく、他のAWSサービスにも接続できそうです。

個人的には、今までComposerをパッケージの読み込みにしか使用していなかったため、クラスファイル読み込み時にも触る必要があるのだと知り大変勉強になりました。


## 参考
- IAMロールの作成
    - [Using temporary credentials with AWS resources](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_use-resources.html)
- namespaceの設定
    - [PHPのオートロード(autoload)](https://qiita.com/atwata/items/5ba72d3d881a81227c2a)
- クライアントオブジェクトをインスタンス化する
    - [クライアントの作成](https://docs.aws.amazon.com/ja_jp/sdk-for-php/v3/developer-guide/getting-started_basic-usage.html)
- テーブル情報を取得する
  - [AWS SDK を使用して DynamoDB テーブルから項目を読み込む
](https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/GettingStarted.ReadItem.html)