LocalStack の S3 を使う場合の注意点

概要

LocalStack は AWS サービスをローカルに構築して、本物の代わりに使えるようにするテスト環境です。

使ってみる

この docker-compose.yml で起動してみます。

docker-compose run main

エンドポイントを LocalStack に向けることでアクセスします。

aws --endpoint=http://localstack:4566 s3 ls

S3 エンドポイントについて

S3 のエンドポイントにはバーチャルホスト形式(virtual hosted-style)とパス形式(path-style)が存在します。パス形式は廃止の方向のようですが、スケジュールについては未定です(2021/05現在)。

形式
バーチャルホスト形式 https://(BucketName).s3.(Region).amazonaws.com
パス形式 https://s3.(Region).amazonaws.com

CLI や SDK で S3 バケットにアクセスした場合、デフォルトではバーチャルホスト形式にしたエンドポイントへ要求します。LocalStack では http://(BucketName).localhost.localstack.cloud:4566/(ObjectKey) という形になります。

対策 1 : パス形式を使用する。

https://github.com/localstack/localstack/issues/3943


@whummer Thank you for your advice.

I tried using endpoint http://s3.localhost.localstack.cloud:4566, but the bucket is not created. It's fine with use_path_style_endpoint, but I prefer not to use it if possible because of deprecation.

How to reproduce

Create docker-compose.yml.

version: '3'
services:
  localstack:
    image: localstack/localstack:0.12.10
    environment:
      - DEFAULT_REGION=ap-northeast-1
      - SERVICES=s3
      - DEBUG=1
    hostname: localstack
    networks:
      default:
        aliases:
          - s3.localhost.localstack.cloud
          - test.s3.localhost.localstack.cloud
          - test.test.s3.localhost.localstack.cloud
  main:
    image: php:7.4.18-cli
    links:
      - localstack:localstack

Run containers.

docker-compose run main bash

CreateBucket (success, but bucket not created.)

$s3 = new Aws\S3\S3Client([
    'version' => '2006-03-01',
    'region' => 'ap-northeast-1',
    'endpoint' => 'http://s3.localhost.localstack.cloud:4566',
]);
$s3->createBucket([
    'Bucket' => 'test.test',
]);

CreateBucket (successfully created.)

$s3 = new Aws\S3\S3Client([
    'version' => '2006-03-01',
    'region' => 'ap-northeast-1',
    'endpoint' => 'http://s3.localhost.localstack.cloud:4566',
    'use_path_style_endpoint' => true,
]);
$s3->createBucket([
    'Bucket' => 'test.test',
]);

PutObject failure

After I created test.test bucket with use_path_style_endpoint, I tried upload object to endpoint http://s3.localhost.localstack.cloud:4566, but 500 error occured.

$s3 = new Aws\S3\S3Client([
    'version' => '2006-03-01',
    'region' => 'ap-northeast-1',
    'endpoint' => 'http://s3.localhost.localstack.cloud:4566',
]);
$s3->putObject([
    'Bucket' => 'test.test',
    'Key' => 'test.txt,
    'SourceFile' => 'test.txt',
]);

Stack trace is:

2021-05-02 23:06:52,870:API: 127.0.0.1 - - [02/May/2021 23:06:52] "PUT /test.txt HTTP/1.1" 500 -
2021-05-02 23:06:52,894:API: Error on request:
Traceback (most recent call last):
  File "/opt/code/localstack/.venv/lib/python3.8/site-packages/werkzeug/serving.py", line 323, in run_wsgi
    execute(self.server.app)
  File "/opt/code/localstack/.venv/lib/python3.8/site-packages/werkzeug/serving.py", line 312, in execute
    application_iter = app(environ, start_response)
  File "/opt/code/localstack/.venv/lib/python3.8/site-packages/moto/server.py", line 177, in __call__
    return backend_app(environ, start_response)
  File "/opt/code/localstack/.venv/lib/python3.8/site-packages/flask/app.py", line 2464, in __call__
    return self.wsgi_app(environ, start_response)
  File "/opt/code/localstack/.venv/lib/python3.8/site-packages/flask/app.py", line 2450, in wsgi_app
    response = self.handle_exception(e)
  File "/opt/code/localstack/.venv/lib/python3.8/site-packages/flask_cors/extension.py", line 165, in wrapped_function
    return cors_after_request(app.make_response(f(*args, **kwargs)))
  File "/opt/code/localstack/.venv/lib/python3.8/site-packages/flask_cors/extension.py", line 165, in wrapped_function
    return cors_after_request(app.make_response(f(*args, **kwargs)))
  File "/opt/code/localstack/.venv/lib/python3.8/site-packages/flask/app.py", line 1867, in handle_exception
    reraise(exc_type, exc_value, tb)
  File "/opt/code/localstack/.venv/lib/python3.8/site-packages/flask/_compat.py", line 39, in reraise
    raise value
  File "/opt/code/localstack/.venv/lib/python3.8/site-packages/flask/app.py", line 2447, in wsgi_app
    response = self.full_dispatch_request()
  File "/opt/code/localstack/.venv/lib/python3.8/site-packages/flask/app.py", line 1952, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/opt/code/localstack/.venv/lib/python3.8/site-packages/flask_cors/extension.py", line 165, in wrapped_function
    return cors_after_request(app.make_response(f(*args, **kwargs)))
  File "/opt/code/localstack/.venv/lib/python3.8/site-packages/flask_cors/extension.py", line 165, in wrapped_function
    return cors_after_request(app.make_response(f(*args, **kwargs)))
  File "/opt/code/localstack/.venv/lib/python3.8/site-packages/flask/app.py", line 1821, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/opt/code/localstack/.venv/lib/python3.8/site-packages/flask/_compat.py", line 39, in reraise
    raise value
  File "/opt/code/localstack/.venv/lib/python3.8/site-packages/flask/app.py", line 1950, in full_dispatch_request
    rv = self.dispatch_request()
  File "/opt/code/localstack/.venv/lib/python3.8/site-packages/flask/app.py", line 1936, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/opt/code/localstack/.venv/lib/python3.8/site-packages/moto/core/utils.py", line 156, in __call__
    result = self.callback(request, request.url, {})
  File "/opt/code/localstack/.venv/lib/python3.8/site-packages/moto/s3/responses.py", line 256, in ambiguous_response
    return self.bucket_response(request, full_url, headers)
  File "/opt/code/localstack/.venv/lib/python3.8/site-packages/moto/s3/responses.py", line 265, in bucket_response
    response = self._bucket_response(request, full_url, headers)
  File "/opt/code/localstack/.venv/lib/python3.8/site-packages/moto/s3/responses.py", line 311, in _bucket_response
    return self._bucket_response_put(
  File "/opt/code/localstack/.venv/lib/python3.8/site-packages/moto/s3/responses.py", line 773, in _bucket_response_put
    if self._create_bucket_configuration_is_empty(body):
  File "/opt/code/localstack/.venv/lib/python3.8/site-packages/moto/s3/responses.py", line 645, in _create_bucket_configuration_is_empty
    create_bucket_configuration = xmltodict.parse(body)[
  File "/opt/code/localstack/.venv/lib/python3.8/site-packages/xmltodict.py", line 327, in parse
    parser.Parse(xml_input, True)
xml.parsers.expat.ExpatError: unclosed token: line 1, column 0