0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Webアプリケーションコンテナ(Laravel)からLocalStackコンテナのS3にファイルをアップロードできるようにする

Last updated at Posted at 2023-07-31

今参画しているプロジェクトでは、ファイルアップロードの機能があり、そのアップロード先がS3でした。

その機能を実装した人はもうプロジェクトにいないので、ローカル環境でどのように動作確認をしていたのかは謎なのですが、「ローカル環境でもS3アップロードの動作ができるようにしたいな~」と思い立ったのでLocalStackを使ってあれこれやってみたらできたので、自分のための備忘録として記載しておきます。

ちなみにAWSLocalStackに関してはズブの素人です。chatGPTさんのお世話になりながらやったらできました(小並)。

以下の環境で動作確認を行いました。

(WSL)
WSL バージョン: 1.2.5.0
カーネル バージョン: 5.15.90.1
Ubuntu 20.04.6 LTS
(Docker)
Docker version 24.0.2
Docker Compose version v2.18.1
(PHP)
PHP 8.2.8
Laravel Framework 10.16.1

コンテナの用意

Webアプリケーションコンテナを用意する

Webアプリケーションコンテナを用意します。今回は自分が使い慣れているLaravelが動くコンテナを用意します。

以下のDocker環境を利用させていただきました:bow_tone1:

この時点のdocker-compose.ymlは以下です。

docker-compose.yml

webコンテナのポート80番がぶつかったので、8080と変更した以外は参照リポジトリのままです。

docker-compose.yml
volumes:
  db-store:
  psysh-store:

configs:
  db-config:
    file: ./infra/docker/mysql/my.cnf

services:
  app:
    build:
      context: .
      dockerfile: ./infra/docker/php/Dockerfile
      target: ${APP_BUILD_TARGET:-development}
    volumes:
      - type: bind
        source: ./src
        target: /workspace
      - type: volume
        source: psysh-store
        target: /root/.config/psysh
        volume:
          nocopy: true
    environment:
      - APP_DEBUG=${APP_DEBUG:-true}
      - APP_ENV=${APP_ENV:-local}
      - APP_URL=${APP_URL:-http://localhost}
      - LOG_CHANNEL=${LOG_CHANNEL:-stderr}
      - LOG_STDERR_FORMATTER=${LOG_STDERR_FORMATTER:-Monolog\Formatter\JsonFormatter}
      - DB_CONNECTION=${DB_CONNECTION:-mysql}
      - DB_HOST=${DB_HOST:-db}
      - DB_PORT=${DB_PORT:-3306}
      - DB_DATABASE=${DB_DATABASE:-laravel}
      - DB_USERNAME=${DB_USERNAME:-phper}
      - DB_PASSWORD=${DB_PASSWORD:-secret}

  web:
    build:
      context: .
      dockerfile: ./infra/docker/nginx/Dockerfile
    ports:
      - target: 8080
        published: ${WEB_PUBLISHED_PORT:-8080}
        protocol: tcp
        mode: host
    volumes:
      - type: bind
        source: ./src
        target: /workspace

  db:
    build:
      context: .
      dockerfile: ./infra/docker/mysql/Dockerfile
    ports:
      - target: 3306
        published: ${DB_PUBLISHED_PORT:-3306}
        protocol: tcp
        mode: host
    configs:
      - source: db-config
        target: /etc/my.cnf
    volumes:
      - type: volume
        source: db-store
        target: /var/lib/mysql
        volume:
          nocopy: true
    environment:
      - MYSQL_DATABASE=${DB_DATABASE:-laravel}
      - MYSQL_USER=${DB_USERNAME:-phper}
      - MYSQL_PASSWORD=${DB_PASSWORD:-secret}
      - MYSQL_ROOT_PASSWORD=${DB_PASSWORD:-secret}

  mailpit:
    image: axllent/mailpit
    ports:
      - target: 8025
        published: ${MAILPIT_PUBLISHED_PORT:-8025}
        protocol: tcp
        mode: host

また、ディレクトリ構成はこのようになっています。infra/dockerディレクトリ以下に各コンテナごとの設定ファイルがあります。
screenshot_02.jpg

LocalStackのコンテナを追加する

先程のdocker-compose.yml公式を参考にしつつ、LocalStackのコンテナを追加します。

docker-compose.yml(追記)
+  aws:
+    build: 
+      context: .
+      dockerfile: ./infra/docker/aws/Dockerfile
+    ports:
+      - "127.0.0.1:4566:4566"            # LocalStack Gateway
+      - "127.0.0.1:4510-4559:4510-4559"  # external services port rang
+    environment:
+      - DEBUG=${DEBUG-}
+    volumes:
+      - "./infra/docker/aws/s3/scripts:/etc/localstack/init/ready.d"
+      - "${LOCALSTACK_VOLUME_DIR:-./infra/docker/aws/volume}:/var/lib/localstack"

なおLambdaを使用する場合は/var/run/docker.sockをボリュームとしてマウントする必要がある1、とのことですが今回Lambdaは使用しないためマウントしていません。

volumes./infra/docker/aws/s3/scriptについては後述します。

LocalStack用のディレクトリを用意

先程のinfra/dockerディレクトリ以下に、awsディレクトリを用意します。

cd infra/docker
mkdir aws

awsディレクトリにはs3/scriptsvolumeディレクトリを用意しておきます。また、s3/scriptsにはinit.shも用意します。

cd aws
mkdir -p s3/scripts
mkdir volume
touch s3/scripts/init.sh
# init.shが実行権を持っているか確認してください
ls -al s3/scripts/init.sh
# 実行権を持っていなかった場合は実行権が必要です(以下はrootユーザー権限で実行)
chmod u+x s3/scripts/init.sh

今ディレクトリはこのようになっています。赤枠が追加した部分です。

screenshot_03.jpg

AWSリソースの初期化スクリプトを作成する

以下で紹介されていたやりかたを参考にし、AWSリソース(S3)の初期化スクリプトを作成します。

先程の作成したinit.shにこのように追記しました。
今回はバケット直下ではなく、その下にディレクトリ(キー)を作ってアップロードしたいためその設定も追加しています。

init.sh
#!/bin/bash

readonly BUCKET_NAME="sample-bucket"
# S3にバケットを作成してファイルを初期化
awslocal s3 mb "s3://${BUCKET_NAME}"
awslocal s3 ls "s3://${BUCKET_NAME}"

# ファイルアップロード先作成
awslocal s3api put-object --bucket "${BUCKET_NAME}" --key uploads/

# AWS CLIのプロファイルを設定
aws configure --profile localstack <<EOF
dummy          # AWS Access Key ID
dummy          # AWS Secret Access Key
us-east-1      # Default region name
json           # Default output format
EOF

docker-compose.ymlappコンテナのenvironmentAWS関連の設定も追加しておきます。

docker-compose.yml(追記)
services:
  app:
    # 省略
    environment:
      # 省略
      - DB_PASSWORD=${DB_PASSWORD:-secret}
+      - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-dummy}
+      - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-dummy}
+      - AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION:-us-east-1}
+      - AWS_BUCKET=${AWS_BUCKET:-sample-bucket}
+      - AWS_USE_PATH_STYLE_ENDPOINT=${AWS_USE_PATH_STYLE_ENDPOINT:-true}

docker composeコマンドでbuildupに成功し、localstack(今回はawsと指定したのでawsと付く名前のコンテナ)コンテナが起動していたらひとまず成功です:smile:

こちらのREADME.mdを参考に、Laravelをインストールし、ブラウザからLaravelのwelcomeページが確認できるようにしておきます。

ファイルをLocalStackコンテナのS3にアップロードできるようにする

以降はLaravelのwelcomeページが確認できるようになっている前提で進みます。

AWS SDK for PHP のインストール

まずAWS SDK for PHPをインストールします。

composer require aws/aws-sdk-php
インストールされたSDKのバージョンが2系だった場合

自分が↑このコマンドだけでインストールを試みたところ、SDKのバージョンが2系だったので、composer.jsonでバージョンを指定し(guzzlehttpもバージョンの指定が必要でした)、composer updateしたら成功しました:rolling_eyes:

composer.json
{
    "require": {
        "php": "^8.1",
+        "aws/aws-sdk-php": "^3.0",
+        "guzzlehttp/guzzle": "^6.0|^7.0",
+        "guzzlehttp/psr7": "^1.0|^2.0",
+        "guzzlehttp/promises": "^1.4.0",
        "laravel/framework": "^10.10",
        "laravel/sanctum": "^3.2",
        "laravel/tinker": "^2.8"
    },
}
composer update

.envファイルの編集

ローカル環境でアップロードができるように、.envのAWS関連の環境変数を以下のようにします。

.env
AWS_ACCESS_KEY_ID=dummy
AWS_SECRET_ACCESS_KEY=dummy
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=sample-bucket
AWS_USE_PATH_STYLE_ENDPOINT=true
AWS_ENDPOINT=http://aws:4566

AWS_BUCKETの値は自身が設定したバケット名にし、AWS_ENDPOINTawsの部分は、LocalStackコンテナを他の名前にしていたら、その名前にしてください(http://localhost:4566だと上手くいかないです。ちなみにそれになかなか気付けずに私は沼りました:sweat:)。

またconfig/aws.phpを作成し、これらの環境変数をセットしてconfigから読み込めるようにしておきます。

config/aws.php
config/aws.php
<?php

return [
    'access_key_id' => env('AWS_ACCESS_KEY_ID'),
    'secret_access_key' => env('AWS_SECRET_ACCESS_KEY'),
    'default_region' => env('AWS_DEFAULT_REGION'),
    'bucket' => env('AWS_BUCKET'),
    'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT'),
    'endpoint' => env('AWS_ENDPOINT'),
];

Laravelではconfig:cacheコマンドを使用して設定ファイルをキャッシュします。
一度設定がキャッシュされると.envファイルを読み込まなくなるため、env関数はconfigの中でのみ使用しなくてはいけません2

フォームの用意

ファイルをアップロードするだけのviewを用意しておきます。

screenshot_04.jpg

fileUpload.blade.php
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    {{-- CSS部分は省略 --}}
    <title>Document</title>
</head>
<body>
    <h1>ファイルをアップロードする</h1>
    <form action="{{ route('upload') }}" method="post" enctype="multipart/form-data" name="uploadForm">
        @csrf
        <input type="file" name="uploadFile">
        <button type="submit">送信</button>
    </form>
</body>
</html>

アップロード時の処理をするURLも設定しておきます。

web.php
Route::post('upload', [FileUploadController::class, 'create'])->name('upload');

アップロード処理を行うController

とりあえずのサンプルなのでバリデーションチェックなし、アーキテクチャとかも気にせずControllerで全部やっちゃうスタイルになっています。あくまでアップロードの処理だけ参考に...。

S3に慣れていなかったので、最初Keyが何を指すのか分からん!となって困惑しました。)

FileUploadController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Aws\S3\Exception\S3Exception;
use Aws\S3\S3Client;

class FileUploadController extends Controller
{
    // ファイルアップロード先
    private const S3_KEY = 'uploads/';

    private S3Client $s3Client;

    public function __construct()
    {
        $this->s3Client = new S3Client([
            'version'                 => 'latest',
            'region'                  => config('aws.default_region'),
            'endpoint'                => config('aws.endpoint'),
            // パス形式のエンドポイントを使用するかどうか(bool)
            // ローカル環境ではtrueにしないと上手くいきませんでした
            'use_path_style_endpoint' => config('aws.use_path_style_endpoint')
        ]);
    }

    /**
     * 初期表示
     */
    public function index()
    {
        return view('fileUpload');
    }

    /**
     * ファイルアップロード
     */
    public function create(Request $request)
    {
        if (!$request->hasFile('uploadFile')) {
            return;
        }

        $file = $request->file('uploadFile');

        $originalName = $file->getClientOriginalName();
        // 今回はアップロード時のファイル名のままでS3にアップロードしています
        $uploadFileName = self::S3_KEY . $originalName;

        try {
            $this->s3Client->putObject([
                'Bucket'     => config('aws.bucket'),
                'Key'        => $uploadFileName,
                'SourceFile' => $file->getRealPath(),
            ]);
            return redirect()->action([self::class, 'index']);
        } catch (S3Exception $e) {
            echo $e->getAwsErrorMessage() . "\n";
        }
    }
}

ファイルを指定して送信ボタンを押すと...

screenshot_07.jpg

ファイルのアップロードができました!
上のサンプルでは割愛していますが、viewでアップロードファイルの一覧を表示するようにしています。
コウテイペンギンはかわいい。:penguin:

screenshot_05.jpg

ローカル環境でS3にアップロードっぽくできてよかったです。

色々分からない中での手探りだったので、もし「こうした方が良いよ」ということがありましたら教えていただけると幸いです。

  1. Mounting the Docker socket /var/run/docker.sock as a volume is required for the Lambda service. 「Getting Started
    / Installation / Starting LocalStack with Docker-Compose
    」より

  2. Configuration - Laravel 10.x - The PHP Framework For Web Artisans

0
1
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
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?