15
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-11-02

#動機
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

15
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?