LaravelでS3への画像アップロードをLocalStackを使って開発

More than 1 year has passed since last update.


動機

AWS S3にファイルをアップロードするPHP Laravelのシステムの開発で、本物のAWSに開発用のS3環境を作るのが色々とアレなのでローカルに擬似的な環境を構築したい。

できれば、Dockerでその他の環境とまとめて管理したい。

そうだ!LocalStackを使おう!


LocalStackとは

LocalStackは、AWSのモックフレームワーク

AWSの様々なサービスをローカル環境で擬似的に利用することができる。


環境

macOS Sierra 10.12.6

PHP 7.0

Laravel 5.5

LocalStack

Docker 17.06.2-ce


前提


  • Dockerがインストールされている。

  • Laravelが動くWebサーバ用の Docker image がある。


概要


  • HTML画面から画像をアップロードしてAWS S3へ保存する処理のLaravel開発環境を構築する。

  • 基本的に全部ローカルPCのDockerを使う。

  • S3の環境は、LocalStackを使う。


環境構築


docker-compose 構成

(localstacklocalhostと、間違えやすい名前にしてしまったので注意)


docker-compose.yml


# Laravel and LocalStack
version: '2.2'

networks:
aws_net:
driver: bridge
ipam:
driver: default
config:
- subnet: 172.16.238.0/24
gateway: 172.16.238.1

services:

# LocalStack
localstack:
image: localstack/localstack:latest
container_name: localstack
environment:
- SERVICES=s3
- DEFAULT_REGION=us-east-1
networks:
- aws_net
ports:
- "8080:8080"
- "4567-4578:4567-4578"

# awscli
awscli:
image: xueshanf/awscli
environment:
- AWS_DEFAULT_REGION=ap-northeast-1
- AWS_DEFAULT_OUTPUT=json
- AWS_ACCESS_KEY_ID=ando
- AWS_SECRET_ACCESS_KEY=mizue
extra_hosts:
- "localstack:172.16.238.1"
- "ando-test.localstack:172.16.238.1"
depends_on:
- localstack
entrypoint: aws --endpoint-url=http://localstack:4572 s3 mb s3://ando-test/

# web - web
web1:
image: phpweb :latest
container_name: web1
environment:
- AWS_DEFAULT_REGION=ap-northeast-1
- AWS_DEFAULT_OUTPUT=json
- AWS_ACCESS_KEY_ID=ando
- AWS_SECRET_ACCESS_KEY=mizue
volumes:
- "./apache/conf.d/httpd-web.conf:/etc/httpd/conf.d/httpd-web.conf"
- "../repo/localstacktest/:/var/www/project/"
ports:
- "8090:8090"
extra_hosts:
- "localstack:172.16.238.1"
- "ando-test.localstack:172.16.238.1"
depends_on:
- localstack



  • Laravel(web1)からLocalStackへアクセスしようとすると、ホスト名解決で問題が起こるので、extra_hosts でhostsを追加している。

    172.16.238.1 が、LocalStackに割り当てているネットワークのIP。

    S3アップロードの時に、{バケット名}.{aws s3のエンドポイント} というホストで接続するようなので、それも入れておく。


  • environment に指定しているAWS関連の値は、特にどこかに登録してあるものではなく、適当な文字列でも大丈夫らしい。


  • サービス名 awscliは、LocalStackが起動してから、バケットを作成するコマンドを送信するためだけのサービス。image xueshanf/awscliに aws cliが入っているのでそれを利用。

    LocalStackのコンテナ起動でやろうとしたけど、うまくいかなかったので別サービスにした。


  • imageのphpweb は、CentOS7 + Apache + PHP7.0 + aws cli などが入っている独自のイメージ。


  • Laravelプロジェクトは、'web1' の /var/www/ の下で create project して作成済み。



動かしてみる

コンテナ立ち上げ

$ docker-compose up -d

Creating network "docker_default" with the default driver
Creating network "docker_aws_net" with driver "bridge"
Creating localstack ...
Creating localstack ... done
Creating docker_awscli_1 ...
Creating web1 ...
Creating docker_awscli_1
Creating docker_awscli_1 ... done

立ち上がったか確認

$ docker ps -a

CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3612e83f6d87 xueshanf/awscli "aws --endpoint-ur..." 6 minutes ago Exited (0) 5 minutes ago docker_awscli_1
53c39d930ca8 phpweb:latest "/bin/sh -c '/usr/..." 6 minutes ago Up 6 minutes 0.0.0.0:8090->8090/tcp web1
586886cdb457 localstack/localstack:latest "/usr/bin/supervis..." 6 minutes ago Up 6 minutes 0.0.0.0:4567-4578->4567-4578/tcp, 0.0.0.0:8080->8080/tcp, 4579-4582/tcp localstack

バケットができているはずなので確認してみる。

Webサーバのコンテナで、aws cliで確認。

--endpoint-url に、今回のLocalStackのS3エンドポイントを指定するだけで、あとは通常のAWSと同じ。

(aws configure を求められたら、適当な文字列を入れておけばいいみたい)

LocalStackのエンドポイントはサービスによって決まっていて、s3は http://LocalStackのホスト:4572 になるので、今回は、http://localstack:4572


$ docker exec -it web1 /bin/bash

[root@1ac2dc002a22 project]# aws --endpoint-url=http://localstack:4572 s3 ls
2006-02-04 01:45:09 ando-test

できてる!

中身は空っぽ!

LocalStackにはブラウザで確認できるダッシュボードがあるので、それも見てみると、バケットが現れた。

locakstack.png


Laravelのコード


AWS(LocalStack)の設定

エンドポイントは 'endpoint' => env('AWS_S3_ENDPOINT'), を追加することで変更できる。


(抜粋)app/config/filesystems.php


's3' => [
'driver' => 's3',
'key' => env('AWS_KEY'),
'secret' => env('AWS_SECRET'),
'region' => env('AWS_REGION'),
'bucket' => env('AWS_BUCKET'),
'endpoint' => env('AWS_S3_ENDPOINT'),
],


環境設定

AWS_BUCKETAWS_S3_ENDPOINT は正確に。

あとは適当で良いみたい。


(抜粋).env


WS_KEY=ando
AWS_SECRET=mizue
AWS_REGION=tokyo
AWS_BUCKET=ando-test
AWS_S3_ENDPOINT=http://localstack:4572



Route


(抜粋)routes/web.php


Route::get('photo', 'PhotoController@index');
Route::post('photo', 'PhotoController@upload');



Controller


app/Http/Controllers/PhotoController.php


<?php

namespace App\Http\Controllers;

use Storage;
use Request;

use App\Http\Requests\PhotoRequest;

class PhotoController extends Controller
{

/**
* ファイル一覧表示
*
*/

public function index()
{
// バケットの中のファイル名一覧取得
$files = Storage::disk('s3')->files();

// ファイルリスト生成
$list = [];
foreach ($files as $file) {
$date = Storage::disk('s3')->lastModified($file);
$list[$date] = [
'name' => $file,
'date' => date("Y-m-d H:i:s", $date),
'type' => pathinfo($file, PATHINFO_EXTENSION),
'size' => Storage::disk('s3')->size($file),
];
}
// 日付降順にソート
krsort($list);

// 画面表示
return view('photo.index')->with(['list' => $list]);
}

/**
* アップロードファイルをs3に保存する
*
*/

public function upload(PhotoRequest $request)
{
// アップロードされたファイルを取得
$file = Request::file('photo');
$image_data = file_get_contents($file->getRealPath());

// s3へ保存
Storage::disk('s3')->put($file->getClientOriginalName(), $image_data, 'public');

// 一覧画面へ
return redirect('/photo');
}

}



app/Http/Requests/PhotoRequest.php


<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

class PhotoRequest extends FormRequest
{

/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/

public function authorize()
{
return true;
}

/**
* Get the validation rules that apply to the request.
*
* @return array
*/

public function rules()
{
return [
'photo' => ['required', 'image', 'max:1024'], // 必須, 画像のみ, 1024KBまで
];
}

}



View


resources/views/photo/index.blade.php


<!doctype html>
<html lang="ja">
<head>
<title>LocalStack S3 test</title>
<link rel="stylesheet" href="/bootstrap-3.3.7/css/bootstrap.min.css">
<script src="/js/jquery-3.2.1.min.js"></script>
<script src="/bootstrap-3.3.7/js/bootstrap.min.js"></script>
</head>
<body>
<header>
@if ($errors->any())
<div class="alert alert-danger">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
</header>
<section id="form">
<div class="panel panel-default">
<div class="panel-body">
<img id="preview" class="img-thumbnail" src="" width="150px">
<form method="post" action="/photo" enctype="multipart/form-data">
{{ csrf_field() }}
<input id="target" type="file" name="photo" multiple>
<input class="btn btn-primary btn-lg" type="submit" value="UPLOAD!!">
</form>
</div>
</div>
</section>
<section id="list">
@foreach ($list as $photo)
<hr />
<div class="media">
<a class="media-left" href="#">
{{-- 画像 --}}
<img width="100px" class="media-object img-thumbnail" src="{{env('AWS_S3_BASE_URL')}}/{{env('AWS_BUCKET')}}/{{$photo['name']}}">
</a>
<div class="media-body">
{{-- 画像情報 --}}
<p>date : {{ $photo['date'] }}</p>
<p>name : {{ $photo['name'] }}</p>
<p>size : {{ ($photo['size'] > (1024*1024)) ? round($photo['size']/1024/1024,2).'MB' : round($photo['size']/1024).'KB'}} </p>
</div>
</div>
@endforeach
</section>
<footer>
<hr />
</footer>
<script>
$(function(){
{{-- 選択画像のプレビュー --}}
$("#target").change( function() {
var file = this.files[0];
fileReader = new FileReader();
fileReader.onload = function(event) {
$("#preview").attr('src', event.target.result);
};
fileReader.readAsDataURL(file);
});
});
</script>
</body>
</html>



テストコード


Tests/Feature/PhotoTest.php


<?php

namespace Tests\Feature;

use Tests\TestCase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;

class PhotoTest extends TestCase
{

/**
* 画像アップロード
*/

public function testUpload()
{
Storage::fake('s3');

// 正常
$response = $this->post('/photo', [
'photo' => UploadedFile::fake()->image('pic1.jpg')
]);
Storage::disk('s3')->assertExists('pic1.jpg');

// エラー:サイズ
$response = $this->post('/photo', [
'photo' => UploadedFile::fake()->image('pic2.jpg')->size(1024*2)
]);
Storage::disk('s3')->assertMissing('pic2.jpg');

// エラー:ファイルタイプ
$response = $this->post('/photo', [
'photo' => UploadedFile::fake()->create('pic3.pdf')
]);
Storage::disk('s3')->assertMissing('pic3.pdf');

}

}



Test


[root@1ac2dc002a22 project]# ./vendor/bin/phpunit

PHPUnit 6.3.0 by Sebastian Bergmann and contributors.

... 3 / 3 (100%)

Time: 2.13 seconds, Memory: 16.00MB

OK (3 tests, 5 assertions)

ここでは、実際にファイルをアップロードしているわけではないので・・


実際に画像をアップロードしてみる

アップロード前。

バケットには何もない。

[root@0e0e1473903e project]# aws --endpoint-url=http://localstack:4572 s3 ls s3://ando-test/ 

アップロードする。

up.png

アップロード後の画面。

af.png

アップした画像をブラウザ表示できてる。

aws cliでも確認。

アップしたファイルがある。

[root@0e0e1473903e project]# aws --endpoint-url=http://localstack:4572 s3 ls s3://ando-test/ 

2017-09-22 19:12:19 903944 mizue.jpg

できた!

これでファイルアップし放題!


注意

dockerを終了させるとアップしたファイルはバケットごと消えます。


参照

LocalStack(Docker Hub)

LocalStack(Git Hub)

Laravel 5.5 HTTP Tests