Laravel で Amazon S3 + CloudFront を代替する

こんにちは❗フリーランス・エンジニアの 九保すこひ です。

さてさて、現在できるだけAWSを体験するようにしているのですが、以前から1点だけ気になることがありました。

これまでクライアントさんにAWSを提案すると「おっ、クラウドですね👍」に続いてよく出てくる言葉があります。

それは・・・・・・

「でも、AWSは料金が心配ですよね・・・」

です。

つまり、AWSは特定のサービスを除いて従量課金が基本ですし、料金が細分化しているので金額が読みにくいことも理由のひとつかと思います。

また、現在は一部で「オンプレミス回帰(≒脱クラウド)」という流れがあるようなのですが、この理由も「セキュリティ」と「コスト」の2つが大きいようです。

📝 参考URL: 海外で進む「オンプレミス回帰」 その背景に何があるのか

なお、さらに言うと個人的にはAmazon S3 + CloundFrontで「許可のある人しか表示できないURL」機能をつくるつもりでした。

しかし、ちょっと調べるとすでに先人たちが素晴らしい記事をいくつも公開しているので、「勝てない勝負には行かない」方式で今回は逆に「脱クラウド」をテーマに記事を書くことにしました。

ということで、今回は別サーバー(サブドメイン)をストレージ用サーバーとして Laravelに「S3 + CloudFront」を代替させ、さらに署名つきURLで権限のある人だけに画像を表示するようにしてみます。

ぜひ参考になりましたら嬉しいです😊✨

「個人的には S3 + CloudFront も
体験してみました👍」

開発環境: Laravel 8.x

今回の実装するメリット/デメリット

今回は「脱クラウド」がテーマですが、やはりメリット&デメリットが両方あってやはりご希望によって使い分けをしたほうがいいと考えています。

ということで、メリットとデメリットをまとめてみることにしました。

メリット

  • 格安レンタルサーバーでもOKなのでコストダウンが見込める
  • 通常のサーバーは月額、年額で固定なので比較的コストがかからない
  • Laravel を使えばそれほど難しいコードは必要ない(さすがに初心者では難しいかもですが・・・)

デメリット

  • S3のスケーラビリティ(≒負荷上昇ヘッチャラ&容量無制限)を失うことになる
  • AWS Lambda など他のサービスと直接連携できなくなる
  • レンタルサーバーの規約によっては「ファイル置き場」としての利用ができない場合がある

実装する方法

まず、Amazon S3 + CloudFrontではどうやって権限つき画像公開をしているかですが、シンプル言うと以下のようになっています。

  1. 直接ブラウザから S3 にアクセスができないようにする
  2. 代わりに CloudFront を入り口にし、署名(トークン)がOKなら S3 のファイルにアクセスできるようにする
  3. 署名付きURLを作ってアクセス(有効時間も指定OK)

つまり、これと同じことをLaravelでやってやればいいということになります。

前提として

今回は「メインサイト」と「ストレージサイト」の2つに分けての開発となります。各情報は以下のようにして話を進めますが、適当にドメインはご自身のものと入れ替えてください。

メインサイト

  • ドメインは www.example.com
  • こちらから SFTP を使って画像サーバーにファイルをアップロードして運用する

ストレージサイト

  • 画像や動画を提供するだけの「ファイル置き場」
  • ドメインは storage.example.com ※1
  • 署名付きでないと画像にアクセスできない

では、実際に作業をやってみましょう❗

ストレージサイトをつくる

まず、画像や動画を提供するストレージサイトを作っていきます。

準備する

「メインサイト」と「ストレージサイト」にはそれぞれLaravelが存在することになりますが、署名付きURLを使うので、暗号キーは同じである必要があります。

そのため、.env内のAPP_KEYは2つとも同じものにしておいてください。

APP_KEY=****************************

コントローラーをつくる

まずは署名付きで画像を表示するためのコントローラーをつくります。
以下のコマンドを実行してください。

php artisan make:controller SignedImageController

するとファイルが作成されますので、中身を以下のように変更します。

app/Http/Controllers/SignedImageController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class SignedImageController extends Controller
{
    public function show(Request $request, $filename) {

        if($request->hasValidSignature()) { // 認証成功

            $path = storage_path('app/signed/images/'. $filename);

            if(\File::exists($path)) {

                return response()->file($path);

            }

        }

        abort(404);

    }
}

この中でやっていることは、まず署名付きURLが本当に妥当なのかをhasValidSignature()でチェックしています。

ちなみにこのメソッドはLaravelに最初から用意されているもので、実際にはURL::signedRoute()URL::emporarySignedRoute()とペアで利用されます。

そして、指定されたファイルがちゃんと存在しているかをチェックし、見つかったらレスポンスでそのファイルを返しています。

ルートをつくる

では、先ほどのコントローラーをルートで指定します。

<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\SignedImageController; // 👈 ここを追加しました

// 省略

Route::prefix('signed')->group(function(){ // 👈 ここを追加しました

    Route::get('image/{filename}', [SignedImageController::class, 'show'])->name('image.signed');

});

Route::get('test/url', function(){ // ここはテストURLをつくるためのものです

    $dt = now()->addSecond(30); // 有効期限は30秒だけ
    return URL::temporarySignedRoute('image.signed', $dt, [
        'user.png'
    ]);

});

⚠ ご注意: なお、太字になっていない方のルートはテスト用URLをつくるものです。本番環境では削除しておいてください。

これで、例えば「/storage/app/signed/images/user.png」という画像を用意し、「https://storage.example.com/signed/image/user.png?expires=*******&signature=**********」にアクセスすると画像が表示されることになります。

さらに有効期限が切れると404ステータスコードが返ることになります👍

メインサイトをつくる

準備する

汎用的にしておきたいので、.envにストレージサイトのトップページを変更できるようにしておきます。URLは適宜変更してください。

.env

STORAGE_ORIGIN=http://storage.example.test

ストレージ用モデルをつくる

次に、ストレージサイトへアクセスするための「署名付きURL」を簡単に取得できるように専用モデルをつくっておきましょう。

以下のコマンドを実行してください。

php artisan make:model Storage

するとファイルが作成されるので、中身を以下のように変更してください。

app/Models/Storage.php

<?php

namespace App\Models;

use Carbon\Carbon;

class Storage
{
    public static function getSignedUrl($uri, $expiration = null) {

        $parameters = [];

        if($expiration) {

            $parameters += [
                'expires' => (new Carbon($expiration))->timestamp
            ];

        }

        ksort($parameters);

        $key = config('app.key');
        $origin = env('STORAGE_ORIGIN');
        $base_url = $origin .'/signed/'. $uri;
        $url = $base_url;

        if(!empty($parameters)) {

            $url .=  '?'. http_build_query($parameters);

        }

        $signature =  hash_hmac('sha256', $url, $key);
        $parameters += ['signature' => $signature];
        return $base_url .'?'. http_build_query($parameters);

    }
}

これで以下のようにすると署名付きURLを取得することができます。

有効期限なし

$signed_url = \App\Models\Storage::getSignedUrl('image/user.png');

有効期限あり

$dt = now()->addMinutes(15);
$signed_url = \App\Models\Storage::getSignedUrl('image/user.png', $dt);

ストレージサイトへアップロードする

「メインサイト」から「ストレージサイト」へのファイルアップロードはSFTPを想定しています。

以下のページを参考にしてみてください。(ちょっとバージョンが古いですが基本は同じだと思います👍)

📝 参考ページ: Laravel でファイルを外部ストレージ(SFTP)へ保存・取得する全13実例

テストしてみる

では、実際に「署名付きURL」をテスト的につくってストレージサイトの画像を表示してみます❗

先にストレージサイトの「/storage/app/signed/images/user.png」に以下の画像を保存します。(つまりメインサイトからアップロードされたものとして設置しています)

ではメインサイトに戻って、まずは「有効期限なし」の署名URLです。
以下のルートを作ってください。

routes/web.php

Route::get('signed_url', function(){

    return \App\Models\Storage::getSignedUrl('image/user.png');

});

これでブラウザで「http://www.example.com/signed_url」にアクセスします。
すると以下のように署名付きURLが表示されました。

そこで、このURLへ再度アクセスしてみます。

すると・・・・・・

うまく画像が表示されました!

では、うまく署名が働いているかをチェックするためにURLの最後の文字を1つだけ削除して再度アクセスしてみます。

どうなるでしょうか・・・・・??

はい❗
404が返ってきました。

成功です😊✨

では、次に「有効期限つき」のURLです。
先ほどのルートを以下のようにしてください。

Route::get('signed_url', function(){

    $dt = now()->addSeconds(30);
    return \App\Models\Storage::getSignedUrl('image/user.png', $dt);

});

同じく「http://www.example.com/signed_url」にアクセスします。

今度はパラメータにexpiresを含んでいます。
ではこのURLへアクセスしてみます。

すると・・・・・・・・・・・

先ほどと同じく画像がちゃんと表示されました!

では、今回は有効期限つきなので、30秒待って再度アクセスしてみましょう。

結果は・・・・・・

はい❗
有効期限が切れたのでアクセスできませんでした。

全て成功です😊👍✨

企業様へのご提案

「メリット」でも書きましたが、S3のように従量課金ではないのでいくらアクセスが多くなってもレンタルサーバー料金のみで済ませることができるようになります。

また、もしアクセスが増え負荷が増大したとしても構造的には比較的シンプルですのでAmazon S3にも移行しやすいかと思います。

ぜひ興味がございましたら、いつでもお気軽にご連絡ください。m(_ _)m

おわりに

ということで、今回は「脱クラウド」をテーマにして記事をお届けしました。

当初は署名認証はhash_equals()を使うつもりでしたが、すでにLaravelに実装されていることに驚きました。

ちなみに、今回の話とは別になりますが、ルート名を使って自動的に署名付きURLをつくることもできます。

そのため、例えばメールに有効期限がある署名付きURLを含めて送信し、「ある一定期間だけ応募できます」みたいなこともできるというわけですね。

さすがLaravelです👍

では、脱クラウドがどれだけ進むかは全く未知数ですがこの分野も注目していきたいです。

ぜひ皆さんもチャレンジしてみてくださいね。

ではでは〜❗

「LEDライトが枕のすぐ近くにあるので
ノイズがエグいです😫」

開発のご依頼お待ちしております 😊✨ お問い合わせ
また、こちらもお待ちしております。
  • 実案件の開発サポート: 詳細
  • ツイッターのフォロー: 詳細
どうぞよろしくお願いいたします!
このエントリーをはてなブックマークに追加       follow us in feedly