👌

aws-sdk-php-laravelを使って、静的ホスティングしている環境のリダイレクトを自由に設定してみる

2021/08/19に公開

前置き

S3+CloudFrontで静的サイトを配信している。
この環境とは別に管理画面+APIサーバーとしてのLaravelが動いている環境がある。
管理画面側から静的ホスティングしている環境にリダイレクト設定をしたい!
ざっくり要件はこんな感じでした。
今まではEC2を使っていたので、リダイレクトやBasic認証はhtaccessでやっていた。
さあ静的ホスティングの場合はどうしよう。となって個人的に色々検討した結果、これがベストかなと思った方法がCloudFront Functionsを使う方法でした。

他に検討した方法

方法 デメリット
S3でリダイレクト 正規表現が使えない
ALBを使ってリダイレクト 設定するルールが多くなって複雑。100ルールまでしか対応できない。そもそもリダイレクトのためだけにALBを使いたく無い
Lambda@Edge 特になし?CloudFront Functionsより高機能

以前はLambda@Edgeでやるケースが多かったみたいです。がCloudFront Functionsだけで可能ならこちらの方がシンプルだと思います。
Lambda@Edgeより前段で実行されるので、シンプルな機能はCloudFront Functionsで複雑な機能はLambda@Edgeという使い分けや組み合わせができるのも良いですね。
https://dev.classmethod.jp/articles/amazon-cloudfront-functions-release/

本題

今回はLaravelからAWS SDKを使ってCloudFront Functionsを更新してみました。
流れはこんな感じ

  1. DBに登録してあるリダイレクト条件とリダイレクト先のpathを取得
  2. Laravelのviewを元にCloudFrontFunctionsで実行させるコードを作る
  3. AWS SDKを使ってCloudFrontFunctionsを更新

aws-sdk-php-laravelのインストール

{
    "require": {
        "aws/aws-sdk-php-laravel": "^3.0"
    }
}
composer update

AWS Service Providerを追加

config/app.php
'providers' => [
    ~~~~
    /*
    * Package Service Providers...
    */
    Aws\Laravel\AwsServiceProvider::class, // ←追加
~~~
],
'aliases' => [
    'App' => Illuminate\Support\Facades\App::class,
    'Arr' => Illuminate\Support\Arr::class,
    'Artisan' => Illuminate\Support\Facades\Artisan::class,
    'Auth' => Illuminate\Support\Facades\Auth::class,
    'AWS' => Aws\Laravel\AwsFacade::class, // ←追加
]

AWS アクセスキー等の設定

.env
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=

Commandを作成

CLIで実行できるようにCommandクラスを作成します。

php artisan make:command CloudFrontSetting

実行すると、app/Console/Commands配下に作成される。

viewを作成

CloudFront Functionsで実行されるコードの雛形になるviewを作成します。

resources/views/cloudfront_function.blade.php
function handler(event) {
  var request = event.request;
  var headers = request.headers;
  var uri     = request.uri;
  var patterns = @json($settings);
  
  // リダイレクト
  for(var i = 0; i < patterns.length; i++) {
    var el = patterns[i];
    if(uri.match(el.condition)){
      var response = {
        statusCode: 301,
        statusDescription: 'Moved Permanently',
        headers: { location: { value: 'https://[host-name]' + el.to } }
      };
    
      return response;
    }
  }

  return request;
}

重要なのはこちら

var patterns = @json($settings);

Laravel側から配列をJavaScriptで使えるように@jsonを使っています。
その後、配列のデータをループで回して正規表現に一致したら、リダイレクトさせています。
配列の中身はこんな感じを想定。

[
    [
        'condition' => '^\/doc$|\/doc\/'
        'to'        => '/document'
    ],
    [
        'condition' => '^\/img$|\/img\/'
        'to'        => '/images'
    ],
]

ちなみにCloudFrontFunctionではconstやletでの変数定義ができずエラーになるので注意してください。
forEachなども使えないので、ループ処理はfor文で対応する。

CloudFrontFunctionsを作成する

作成はAWS CLIからでもコンソールからでもできます。
https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/functions-tutorial.html

作成した関数名を環境変数に設定します。

.env
CLOUD_FRONT_FUNCTION=TestFunction

CloudFrontSettingコマンドを修正する

先ほど作ったCloudFrontSettingコマンドを変更していきます。

app/Console/Commands/CloudFrontSetting.php
<?php

namespace App\Console\Commands;

use App\Models\Redirect;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\App;

class CloudFrontSetting extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'cloudfront:setting';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'CloudFrontの設定';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return int
     */
    public function handle()
    {
        // パッケージ呼び出し
        $cloudfront       = App::make('aws')->createClient('CloudFront');
        
        // 現在の設定を取得
        $current_setting = $cloudfront->describeFunction([
            'Name' => env('CLOUD_FRONT_FUNCTION'), 
        ]);

        // リダイレクト設定を取得
        $settings = Redirect::select(['condition','to'])->whereActive()->get();

        // CloudFrontFunctionの設定を更新
        $cloudfront->updateFunction([
            'FunctionCode' => View('cloudfront_function',compact('settings')), 
            'FunctionConfig' => [ 
                'Comment' => 'CloudFront環境ごとの設定', 
                'Runtime' => 'cloudfront-js-1.0', 
            ],
            'IfMatch' => $current_setting['ETag'], 
            'Name' => env('CLOUD_FRONT_FUNCTION'), 
        ]);
    }
}

はじめにAWS SDKのクライアントを呼び出します。

$cloudfront = App::make('aws')->createClient('CloudFront');

CloudFront Functionsの更新にはETag(現在のバージョン)が必要なのでdescribeFunctionを使い取得します。

$settings = Redirect::select(['condition','to'])->whereActive()->get();

Modelからリダイレクトの設定を取得します。DBからの取得についての詳細は割愛。
https://readouble.com/laravel/8.x/ja/eloquent.html

updateFunctionFunctionCodeに動かすコードを指定します。今回は先ほど作ったviewを使います。

FunctionCode
function handler(event) {
    var request = event.request;
    var headers = request.headers;
    var uri     = request.uri;
    var patterns = [{"condition":"^\\\/doc$|\\\/doc\\\/","to":"\/document"},{"condition":"^\\\/img$|\\\/img\\\/","to":"\/images"}];

    // リダイレクト
    for(var i = 0; i < patterns.length; i++) {
        var el = patterns[i];
        if(uri.match(el.condition)){
        var response = {
            statusCode: 301,
            statusDescription: 'Moved Permanently',
            headers: { location: { value: 'https://[host-name]' + el.to } }
        };
        
        return response;
        }
    }
        
    return request;
    }

最終的にCloudFront Functionsに更新されるのはこんな感じ。

AWS SDKの詳細

https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-cloudfront-2020-05-31.html#updatefunction
https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-cloudfront-2020-05-31.html#describefunction

まとめ

これで静的ホスティングしている環境にリダイレクト処理をかけれました。
同じく静的ホスティングする時に悩ましいstg環境のBasic認証問題もCloudFront Functions使えば簡単に実装できます。
https://zenn.dev/mallowlabs/articles/cloudfront-functions-basic-auth

今回は管理画面からリダイレクトを変更したりする要件だったので、DBに保存できるようにLaravelで実装しました。
ですが単純にリダイレクトやBasic認証かけるだけならかけるだけならTerraformを使ったりAWS CLI使ったりが便利そうです。

Discussion