megutech

自身の備忘録として主にWEBサーバー周りの技術について投稿しています。

Safariでは206 Partial Contentに対応していないとVideoタグでmp4が再生できない

S3に保存されたmp4動画をPHPを経由して配信していたのだが、iOS/MacSafariだと再生できない事が判明した。

原因はVideoタグなどのリソースはRangeヘッダーを付けてリクエストが投げられるのだが、ここで正しく206 Partial Contentを返してあげないとSafariでは動画を再生できないというものだった。

chromeとかは再生してくれるのにね。流石Safari。すごいぞSafari。滅びればいいのに。

で、以下どうやって対策をとったかという話。

環境

Service Version
PHP 7.2.2
Laravel 6.5.0

問題のコード

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;

class ResourceController extends Controller
{
    public function download()
    {
        return Storage::disk('s3')->download('your/file/path/movie.mp4', 'movie.mp4');
    }
}

対応

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\StreamedResponse;

class ResourceController extends Controller
{
    public function download()
    {
        $disk = Storage::disk("s3");
        $size = $disk->size('your/file/path/movie.mp4');

        $start = 0;
        $status = 200;
        $length = $size;

        $headers = [
            'Content-Type' => 'video/mp4',
            'Content-Length' => $size,
            'Accept-Ranges' => 'bytes'
        ];

        if ($range = Request::server('HTTP_RANGE', false)) {
            list($param, $range) = explode('=', $range);
            if (strtolower(trim($param)) !== 'bytes') {
                abort(400);
            }

            list($from, $to) = explode('-', $range);
            if ($from === '') {
                $end = $size - 1;
                $start = $end - intval($from);
            } else if ($to === '') {
                $start = intval($from);
                $end = $size - 1;
            } else {
                $start = intval($from);
                $end = intval($to);
            }
            $length = $end - $start + 1;

            $headers['Content-Length'] = $length;
            $headers['Content-Range'] = sprintf('bytes %d-%d/%d', $start, $end, $size);
            $status = 206;
        }

        $response = new StreamedResponse(function() use ($disk, $start, $length) {
            $stream = $disk->readStream('your/file/path/movie.mp4');
            fseek($stream, $start, SEEK_SET);
            echo fread($stream, $length);
            fclose($stream);
        }, $status, $headers);

        $response->headers->set(
            'Content-Disposition',
            $response->headers->makeDisposition(
                'attachment',
                "movie.mp4",
                str_replace('%', '', Str::ascii("movie.mp4"))
            )
        );

        return $response;
    }
}

上記は実際に書いたコードを何とか一つにまとめたものです。

動作確認はしてないです。(もちろん上記のようにまとめる前の本チャンコードでは動作確認取れてます。)

まあこんな感じ。

めんどくさ。

無停止でCentOS7 on EC2のEBSストレージを拡張する

遅ればせながら今回の案件で初めてEC2を触ることに。

そこでまずはステージング環境を作ろうとごにょごにょしてたら、ディスクを8GiBで作成してしましました。

ステージング環境とはいえもう少し欲しかったのでディスクを拡張しようとしたらAWSではなんと無停止で拡張可能とのこと!再起動も不要!マジ最高!ってなったんですが、やり方が分からず詰まったりしたので備忘録か。

AWSコンソールからEBSボリュームを増やす

AWSコンソールからEC2のボリュームサイズを変更する。

変更後ステータスがin-use-optimizingが100%になるのを待ちましょう。

いや、待つ必要があるかは分かんないんですが。この辺誰か情報ください。

私は8 -> 40GiBに変更で1時間ほどかかりました。

f:id:citrus_soda:20191121122039p:plain
AWS Console

f:id:citrus_soda:20191121122102p:plain
AWS Console

f:id:citrus_soda:20191121122116p:plain
AWS Console

web上での操作は以上です。

拡張されたボリュームを適用する

拡張が完了したので早速sshログイン後確認してみよう。

$ df -h
ファイルシス   サイズ  使用  残り 使用% マウント位置
/dev/nvme0n1p1   8.0G  896M  7.2G   11% /

しかし拡張されていない。

おかしい・・・。何かが、あったに・・・違いない・・・。

「そう、誰もパーティショニングを拡張していないのである!」

まずは落ち着いてブロックデバイスを見てみる。

$ lsblk
NAME        MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
nvme0n1     259:0    0  40G  0 disk
└─nvme0n1p1 259:1    0   8G  0 part /

lsblkでブロック確認するとdisk側がpartitionより大きくなって、先ほど指定したサイズに変更されていることが分かる。

というわけでnvme0n1のパーティション、nvme0n1p1をgrowpartで拡張する。

nvme0n1p1をどうやって拡張するんだろうと思ったら、nvme0n1のパーティション1(nvme0n1p1)を拡張するので、/dev/nvme0n1 1と指定するみたい。

$ sudo growpart /dev/nvme0n1 1
CHANGED: partition=1 start=2048 old: size=16775168 end=16777216 new: size=83883999,end=83886047
$ lsblk
NAME        MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
nvme0n1     259:0    0  40G  0 disk
└─nvme0n1p1 259:1    0  40G  0 part /

これでパーティションがディスクのサイズまで拡張された。

ただこの時点ではファイルシステムにまで影響していないので、最後にファイルシステムを拡張する。

$ sudo xfs_growfs /dev/nvme0n1p1

最後にちゃんと拡張されているか確認して完了だ。

$ df -h
ファイルシス   サイズ  使用  残り 使用% マウント位置
/dev/nvme0n1p1    40G  897M   40G    3% /

以上ーー!

参考

ダウンタイムゼロでEC2(CentOS7)のEBSボリュームを拡張する方法

Laravel6でMinIOを使う

Laravelではドキュメント通りに必要なパッケージをインストールすればS3はすぐに使うことができるようになる。

ただ開発環境はMinIOで代用したい場合などは、少し工夫が必要となる。

というのもバケット指定方法が違うのだ。

ということで config/filesystems.phps3とは別の設定を追加する。

config/filesystems.php

<?php
return [
     // ~
    'disks' => [
        // ~
        'minio' => [
            'driver' => 's3',
            'endpoint' => env('MINIO_ENDPOINT', 'http://127.0.0.1:9000'),
            'use_path_style_endpoint' => true,
            'key' => env('AWS_ACCESS_KEY_ID'),
            'secret' => env('AWS_SECRET_ACCESS_KEY'),
            'region' => env('AWS_DEFAULT_REGION'),
            'bucket' => env('AWS_BUCKET'),
        ],
    ],
    // ~
];

ここで重要なのが、endpointuse_path_style_endpointである。

これを指定することでMinIOに対応してくれる。

ただしdiskの指定をminioにする必要があるため、ファサードを書き換えてしまったり、何か処理を噛ませるといいだろう。そのあたりは各々のプロジェクトで考えてほしい。

おまけ

参考までにファサード書き換えで本番環境と開発環境でs3の向き先を変える方法を記載しておく。

config/app.php

<?php
return [
    // ~
    'providers' => [
        // ~
        App\Filesystem\FilesystemServiceProvider::class,
        // ~
    ],
    
    'providers' => [
        // ~
        // 'Storage' => Illuminate\Support\Facades\Storage::class,
        'Storage' => App\Filesystem\Storage::class,
        // ~
    ],
    // ~
];

app/Filesystem/FilesystemServiceProvider.php

<?php

namespace App\Filesystem;

use Illuminate\Support\ServiceProvider;

class FilesystemServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->bind('filesystem.expansion', function($app) {
            return new \App\Filesystem\FilesystemManager($app);
        });
    }
    
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
}

app/Filesystem/Storage.php

<?php

namespace App\Filesystem;

use Illuminate\Support\Facades\Storage as OrgStorage;

/**
 * @method static \Illuminate\Contracts\Filesystem\Filesystem disk(string $name = null)
 *
 * @see \Illuminate\Filesystem\FilesystemManager
 */
class Storage extends OrgStorage
{
    /**
     * Get the registered name of the component.
     *
     * @return string
     */
    protected static function getFacadeAccessor()
    {
        return 'filesystem.expansion';
    }
}

app/Filesystem/FilesystemManager

<?php

namespace App\Filesystem;

use Illuminate\Filesystem\FilesystemManager as OrgFilesystemManager;

/**
 * @method static \Illuminate\Contracts\Filesystem\Filesystem disk(string $name = null)
 *
 * @see \Illuminate\Filesystem\FilesystemManager
 */
class FilesystemManager extends OrgFilesystemManager
{
    /**
     * Get a filesystem instance.
     *
     * @param  string|null  $name
     * @return \Illuminate\Contracts\Filesystem\Filesystem
     */
    public function disk($name = null)
    {
        if (config('app.debug') && ($name === 's3')) {
            $name = 'minio';
        }
        return parent::disk($name);
    }
}

S3互換のMinIOをCentOS7にインストールする

S3を使ったサービスを開発したいが、開発中は課金が発生してほしくない。
そんなわがままなあなたにMinIOが答えてくれるだろう。
今回はこのMinIOをCentOS7にインストールし、起動スクリプトを書くまでを記す。

ユーザーの追加

$ sudo useradd minio -s /sbin/nologin

インストール

公式サイトからダウンロード

$ cd /usr/local/src
$ wget https://dl.min.io/server/minio/release/linux-amd64/minio
$ chmod +x minio
$ chown minio:minio minio
$ mv minio /usr/local/bin

systemd用スクリプトを作成

今回はgithubにアップされているスクリプトを活用する。

$ cs /etc/systemd/system/
$ curl -O https://raw.githubusercontent.com/minio/minio-service/master/linux-systemd/minio.service
$ sed -i -e 's/minio-user/minio/' minio.service

データディレクトリを作成

$ mkdir /var/lib/minio
$ chown mino:minio /var/lib/minio
chmod 700 /var/lib/mino

設定ファイルの生成

/etc/default/minio

MINIO_VOLUMES=/var/lib/minio
MINIO_ACCESS_KEY={適当なセキュアな文字列}
MINIO_SECRET_KEY={適当なセキュアな文字列}

サービスの有効化

$ systemctl start minio
$ statemctl status minio
$ systemctl enable minio

Can't resolve 'fs'

browser向けjsの環境構築中、環境によって変えたい値を.envで管理したいという事でdotenv-webpackをインストールしたのだが、bundle作成時にエラーが出てつまったので共有をしておく。

環境

Package Ver
webpack 4.41.0
webpack-cli 3.3.9
webpack-dev-server 3.8.1
dotenv-webpack 1.7.0

経緯

他のプロジェクトでもdotenv-webpackをブラウザ向けに使っていたこともあり、特に何も考えずにdotenv-webpackをインストールしてビルドした。

$ npm install dotenv-webpack

webpack.config.js

const path = require('path');
const Dotenv = require('dotenv-webpack');

module.exports = {
  // ...
  plugins: [
    new Dotenv({ systemvars: true })
  ],
  // ...
};

package.json

{
  // ...
  "script": {
    // ...
    "dev": "webpack-dev-server",
    // ...
  },
  // ...
}
$ npm run dev

すると以下のエラーが発生した。

ERROR in ./node_modules/dotenv/lib/main.js
Module not found: Error: Can't resolve 'fs' in '/xxx/xxx/node_modules/dotenv/lib'
 @ ./node_modules/dotenv/lib/main.js 24:11-24
 @ ./src/api/config.ts
 @ ./src/api/encode.ts
 @ ./src/api/index.ts
 @ ./src/index.ts

よくある間違った解法

Can't resolve 'fs'などでググるwebpack.config.jsのtargetをnodeに変更しましょうという記事が出てくるが、これはサーバーサイドの時の話である。

ブラウザ向けの場合はこの設定をしてしまうと動かない。ではどうすればいいかというと、fsモジュールを使わないようにするしかない。モジュール内で使われているのであれば代替モジュールを検討するほかなくなる。

しかしそれは非常に困る。だって他のプロジェクトではdotenv-webpackちゃんと動いてるし。

解決

css-loaderのissueに同じような問題が上がっていたので、それを参考にwebpack.config.jsnode: { fs: 'empty'}を追加した。

webpack.config.js

const path = require('path');
const Dotenv = require('dotenv-webpack');

module.exports = {
  // ...
  plugins: [
    new Dotenv({ systemvars: true })
  ],
  node: {
    fs: 'empty'
  },
  // ...
};

詳細は公式ドキュメントに記載されているが、要約するとNode用モジュールをポリフィルもしくはモックして、ブラウザーでも実行できるようにする!という事らしい。

そしてemptyは空のオブジェクトを提供するという意味なので、dotenv内のfsは何もしない空モジュールという事になる。

ブラウザ用ではfsなんて使わないので、これで問題無。無事解決。良かった良かった。

npm runに引数を渡そうとしてはまった話

npm run scriptコマンドライン上でオプションを付ける方法が分からず20分くらいはまり、腹が立ったので今後のために残しておく。

経緯

とあるプロジェクトをcloneしてきてwebpack-dev-serverを立てようとしたとき、webpack.config.jsにデフォルトで記述されていたhostとportは自分の環境では使えなかったので、コマンドライン上で起動時にhostとportを指定しようとした。

package.json

{
    // ...
    "scripts": {
        "dev": "webpack-dev-server",
        // ...
    },
    // ...
}
$ npm run dev --host 192.168.xx.xx --port xxxx

しかしこの起動コマンドではwebpack.config.jsに記述されているhostとportで立ち上がってしまった。

調査

まずはnpmを使わず直接webpack-dev-serverで指定した場合の挙動を確認した。

$ ./node_modules/webpack-dev-server/bin/webpack-dev-server.js --host 192.168.xx.xx --port=xxxx

とするとまあ立ち上がる。腹が立つくらい華麗に立ち上がる。

という事はnpm run側の問題だろうという事で公式ドキュメントを覗きに行くと、以下の記述が。

As of npm@2.0.0, you can use custom arguments when executing scripts. The special option -- is used by getopt to delimit the end of the options. npm will pass all the arguments after the -- directly to your script:

sh npm run test -- --grep="pattern"

なるほど。

渡したいコマンドは -- の後にかけ!! という事らしい。

解決

という事で以下のように変更。

$ npm run dev -- --host 192.168.xx.xx --port xxxx

無事動きました!

良かった良かった。

情報

https://docs.npmjs.com/cli/run-script

https://qiita.com/tiny-studio/items/ce28bf84c76aba53122f

https://qiita.com/qrusadorz/items/db042f65be95f34d6271

ムームーメールから送信したメールが迷惑メールに振り分けられる

ムームードメインムームーメールを使っていると、ムームーメールからの送信メールで Received-SPF: softfail になっていた。

という事でDNSspf設定を追加してあげる。

DNSSPF設定追加

ムームードメインのコントロールパネルからムームDNSを選択し、対象のドメインに以下を追加する。

サブドメイン 種別 内容
TXT v=spf1 include:_spf.muumuu-mail.com ~all

以上。