LaravelのカスタムドライバでAssumeRoleしつつS3のデータを簡単に取得できるようにしたい!

はじめに

こんにちは。お久しぶりです。技術統括本部DXビジネス開発部フィールドソリューションユニットの濱村です。
入社時はフロントエンドをメインに開発していましたが今はインフラをしていることが多いです。幅広く技術を触れる環境に毎日感謝して開発しています。

背景

Laravelを使用してS3への画像等のファイルをアップロードやダウンロードをしたいというケースが多いと思います。LaravelでS3とファイルをやりとりするための知見はネットにそこそこあり、特にS3のドライバを用いて簡単にS3とファイルのやりとりをすることができます。ただし、今回は特殊でカスタムドライバを作成する必要があったので紹介します。

カスタムドライバでS3にアクセスする

AWSでは基本的にAccessKeyとSecretAccessKeyがあればAWSリソースにアクセスすることができます。ただし弊社ではAssumeRoleでリソースへのアクセスを制限しており、特定のロールから権限を委譲してもらわなければリソースに対してアクセスできないようになっています。AssumeRoleはリソースの運用とセキュリティ観点から導入しており、各環境毎にロールを切ってアクセスを制御することで1つのAWSアカウントで各環境のリソースを分けて運用できるようにしています。ということで、今回はLaravelのプロセスがAssumeRoleをして権限を委譲してもらう必要があります。
通常、Laravelが提供しているS3にアクセスできるfilesystemモジュールをComposerで持ってきます。しかし、今回はこのS3へのアクセスの際にAssumeRoleの処理を噛ませる必要があるため自前のカスタムドライバを作成します。

ServiceProviderを拡張してProviderを作成

app/Providers/AwsServiceProvider.php

<?php

namespace App\Providers;

class AwsServiceProvider extends ServiceProvider
{
    /**
     * サービスの初期処理登録後に実行
     *
     * @return void
     */
    public function boot()
    {
        Storage::extend('assumeS3', function ($app, $config) {
            $sample_driver = new SampleDriver($config);
            return new AwsSampleAdapter($sample_driver, $config);
        });
    }
}

AssumeRole処理とAdapterの作成

カスタムドライバの枠ができたところでAssumeRoleをする処理を書いていきます。 ここで使用しているModuleがなかなかややこしいところです。 AwsS3V3AdapterでもIlluminate\Filesystem\AwsS3V3AdapterLeague\Flysystem\AwsS3V3\AwsS3V3Adapterの2つあるため要注意です。

Illuminate\Filesystem\AwsS3V3Adapter | Laravel API

Aws S3 (v3) Adapter - Flysystem

詳しい使い方や仕組みについては上記の公式ドキュメントに記載があります。ここでは上記2つのモジュールを組み合わせてAssumeRoleとS3アクセスを実現したコードを紹介します。

app/Providers/AwsServiceProvider.php

<?php

namespace App\Providers;

use Aws\Credentials\AssumeRoleCredentialProvider;
use Aws\Credentials\CredentialProvider;
use Aws\S3\S3Client;
use Aws\Sts\StsClient;
use Illuminate\Filesystem\AwsS3V3Adapter;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\ServiceProvider;
use League\Flysystem\AwsS3V3\AwsS3V3Adapter as FlyS3Adapter;
use League\Flysystem\Filesystem as FlyFilesystem;

class AwsServiceProvider extends ServiceProvider
{
    /**
     * サービスの初期処理登録後に実行
     *
     * @return void
     */
    public function boot()
    {
        Storage::extend('assumeS3', function ($app, $config) {

            $assumeRoleCredentials = new AssumeRoleCredentialProvider([
                'client' => new StsClient(
                    [
                        'version' => 'latest',
                        'region' => $config['region'],
                        'credentials' => [
                            'key' => $config['key'],
                            'secret' => $config['secret'],
                        ],
                    ]
                ),
                'assume_role_params' => [
                    'RoleArn' => $config['arn'],
                    'RoleSessionName' => $config['session'],
                ],
            ]);

            $provider = CredentialProvider::memoize($assumeRoleCredentials);

            $s3Client = new S3Client([
                'region' => $config['region'],
                'version' => 'latest',
                'credentials' => $provider,
            ]);

            $adapter = new FlyS3Adapter($s3Client, $config['bucket']);
            $driver = new FlyFilesystem($adapter);

            return new AwsS3V3Adapter(
                $driver,
                $adapter,
                $config,
                $s3Client,
            );
        });
    }
}

LaravelのProviderを使用するための記述

上記で実装したカスタムドライバが使用できるようにコードを追加していきます。

providersに作成したProviderを登録

config/app.php

'providers' => [
    ...
    App\Providers\AwsServiceProvider::class,
],

filesystemのdisksにassumeS3を追加

config/filesystem.php

'disks' => [

        'local' => [
            'driver' => 'local',
            'root' => storage_path('app'),
            'throw' => false,
        ],

        'public' => [
            'driver' => 'local',
            'root' => storage_path('app/public'),
            'url' => env('APP_URL').'/storage',
            'visibility' => 'public',
            'throw' => false,
        ],

        'assumeS3' => [
            'driver' => 'assumeS3',
            'key' => env('AWS_ACCESS_KEY_ID'),
            'secret' => env('AWS_SECRET_ACCESS_KEY'),
            'arn' => env('AWS_ROLE_ARN'),
            'session' => env('AWS_ROLE_SESSION_NAME'),
            'region' => env('AWS_DEFAULT_REGION'),
            'bucket' => env('AWS_BUCKET'),
        ],

    ],

Accessorの実装

assumeS3のdiskを使ってS3から署名付きURLを発行するメソッドをAccessorを実装
※ AccessorはLaravel公式のものではありません。Controllerでの実装がシンプルにするため独自に実装しています。

app/Accessors/AwsAccessor.php

<?php

namespace App\Accessors;

use DateTime;
use Illuminate\Support\Facades\Storage;

class AwsAccessor
{
    /**
     * getPresignedUrl
     *
     * @return string S3 PreSinedURL
     *
     * @see S3 PreSignedURL https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/ShareObjectPreSignedURL.html
     */
    public static function getPresignedUrl(
        string $fileName,
        string $expiredAt
    ) {

        $data = Storage::disk('assumeS3')->temporaryUrl($fileName, new DateTime($expiredAt));

        return $data;
    }
}

Controllerの実装

app/Http/Controllers/S3Controller.php

public function getPresignedURL()
    {
        $presigned_url = AwsAccessor::getPresignedUrl(config('app.file_path'), config('app.s3_presigned_exp'));

        return $presigned_url;
    }

環境変数からS3関連の変数を定義

config/app.php

's3_presigned_exp' => env('AWS_PRESIGNED_EXP', '+ 30 minute'),
's3_file_path' => env('AWS_FILE_PATH', '/'),

詰まったところ

カスタムドライバを作る際にAWSリソースとやりとりをするモジュールとしてflysystemがあることを知りました。このflysystemが少し厄介でこれの扱いに少し手こずりました。特にAssumeRoleをする機構の知見がなかなか出回っておらずflysystemのドキュメントとにらめっこをしながらあれこれ試してようやくたどり着きました。 ただ、こうやって苦労してAssumeRoleの機構を作れたおかげでファイルを取得するAPIはController部分が1行の記述だけで実装ができています。今回のように特殊なケースでもカスタムのClassを作成することでControllerの記述をスッキリさせることができてとても満足です。

おわりに

オプティムでは、世界の人々・各産業に大きく良い影響を与えたい方、身の丈に合わない大きな志を持って楽しみながら挑戦し、自ら己の可能性を広げられる方、そして、あらゆる属性を意識せず思いやりを持ってこれからのオプティムという組織・文化を創っていって頂ける方を新卒・中途問わず通年で募集しています。新卒採用では引き続きエンジニア志望(プログラミング未経験者可)、ビジネスサイド志望ともに募集しています。私たちと一緒に世界を変える大きなことにチャレンジしたいという方、是非以下をご覧になってください。私もこの理念に共感し、このリンクを踏んだ者の1人です。同じマインドをお持ちの方、一緒にお仕事ができることを楽しみにしております。以上、ご応募お待ちしております!

www.optim.co.jp