AWS SDK for PHP: 認証情報をプロセス間で再利用する

背景

AWS EC2 上で AWS SDK を使ってリソースにアクセスする場合、Instance Profile に指定したロールの権限が自動的に使われます。これは実際には IMDS (Instance MetaData Service) へ HTTP でアクセスして一時的な認証情報を取得することで実現しており、Web サービスではリクエスト(プロセス)毎に発生します。しかしこのリクエスト数には一定の上限があり、スロットリングによるリトライやエラー(タイムアウトになる場合もあるようです)が発生する場合があります。これにより、AWS リソースへのアクセスを頻繁におこなう Web サービスでは問題になることがあります。

参考) Retrieve instance metadata - Amazon Elastic Compute Cloud

PHP のプロセス間で共有する領域に認証情報を有効期限までキャッシュすることで、この問題を回避します。IMDS のリクエストが減ることで、パフォーマンスの向上も見込めるかもしれません。

Doctrine Cache を使う場合

公式ドキュメントには Doctrine Cache を使って APCu にキャッシュする方法が記載されています。が、このライブラリは開発が終了している模様。ApcuCache クラスも最新バージョンでは削除されているため、v1 を使う必要があります。

参考) Configuration for the AWS SDK for PHP Version 3 - AWS SDK for PHP

APCu をインストール (例)

sudo yum install -y php-pecl-apcu

Doctrine Cache (v1) をインストール

composer require doctrine/cache:^1.0

キャッシュ例 (公式ドキュメントより)

$s3 = new Aws\S3\S3Client([
    'version' => 'latest',
    'region' => 'ap-northeast-1',
    'credentials' => Aws\Credentials\CredentialProvider::cache(
        Aws\Credentials\CredentialProvider::defaultProvider(),
        new Aws\DoctrineCacheAdapter(new Doctrine\Common\Cache\ApcuCache())
    ),
]);

Cache Provider を自作

Deprecated なライブラリを使うのも気持ちが悪いので、自作してみました。

Aws\CacheInterface を実装した Cache Provider を作り、CredentialProvider::cache() に指定すれば良いだけです。同様にして、保存先を APCu 以外にすることも容易です(ファイルとか Shared Memory とか)。ちなみに Aws\DoctrineCacheAdapter は Doctrine Cache をラップした Cache Provider の実装になります。

例) 保存先となる APCu のキーはプロセス間で共有されるため、他の用途と重複しないよう namespace を指定できるようにします。

class MyApcuCache implements \Aws\CacheInterface
{
    private string $namespace;

    public function __construct(string $namespace)
    {
        $this->namespace = $namespace;
    }

    private function get_apcu_key(string $key): string
    {
        return sprintf('%s[%s]', $this->namespace, $key);
    }

    public function get($key)
    {
        return apcu_fetch($this->get_apcu_key($key));
    }

    public function set($key, $value, $ttl = 0)
    {
        apcu_store($this->get_apcu_key($key), $value, $ttl);
    }

    public function remove($key)
    {
        apcu_delete($this->get_apcu_key($key));
    }
}

$s3 = new Aws\S3\S3Client([
    'version' => 'latest',
    'region' => 'ap-northeast-1',
    'credentials' => Aws\Credentials\CredentialProvider::cache(
        Aws\Credentials\CredentialProvider::defaultProvider(),
        new MyApcuCache('AwsCredentials')
    )
]);