CLOVER🍀

That was when it all began.

LocalStackとMinIOで、バケットにアップロードしたファイルをHTTPで参照する

これは、なにをしたくて書いたもの?

LocalStackやMinIOをAmazon S3互換のオブジェクトストレージとしてAWS SDKやAWS CLIからアクセスすることはよくあるのですが、
バケットにアップロードしたファイルをHTTPで参照したことってないな、とふと思いまして。

こういう感じで、Webサーバー的に使いたいという話ですね。

Amazon S3 を使用して静的ウェブサイトをホスティングする - Amazon Simple Storage Service

調べたらできそうだったので、ちょっと試してみました。

環境

今回の環境は、こちら。

LocalStack。

$ python3 -V
Python 3.10.6


$ localstack --version
1.3.1

起動。

$ localstack start

MinIOサーバー。

$ minio --version
minio version RELEASE.2023-01-25T00-19-54Z (commit-id=50d58e9b2dbbfbe2f3afa2c16ae701f9c1cf03ec)
Runtime: go1.19.4 linux/amd64
License: GNU AGPLv3 <https://www.gnu.org/licenses/agpl-3.0.html>
Copyright: 2015-2023 MinIO, Inc.

起動。

$ minio server /var/lib/minio/data --console-address :9001

MinIOのCLI。

$ mcli --version
mcli version RELEASE.2023-01-11T03-14-16Z (commit-id=14c2e506fa78b53fb6db88bcf87d8f6d3fb6989e)
Runtime: go1.19.4 linux/amd64
Copyright (c) 2015-2023 MinIO, Inc.
License GNU AGPLv3 <https://www.gnu.org/licenses/agpl-3.0.html>

MinIOのCLIからアクセスする時は、myminioとエイリアスしておきます。ユーザー名とパスワードはminioadmin/minioadminです。

$ mcli alias set myminio http://localhost:9000 minioadmin minioadmin

AWS CLIも使います。

$ aws --version
aws-cli/2.9.18 Python/3.9.11 Linux/5.15.0-58-generic exe/x86_64.ubuntu.22 prompt/off

クレデンシャルは、環境変数で指定することにします。

$ export AWS_ACCESS_KEY_ID=minioadmin
$ export AWS_SECRET_ACCESS_KEY=minioadmin
$ export AWS_DEFAULT_REGION=ap-northeast-1

LocalStackの場合はアクセスキーIDとシークレットアクセスキーの値はなんでもいいのですが、同じものを使うことにします。
※LocalStack用のAWS CLIは、今回は外しておきます

バケットにアップロードしたファイルを、誰からも参照可能にする

LocalStack+AWS CLIで試す

まずは、LocalStackAWS CLIで操作してみましょう。

バケット作成。LocalStackをローカルで起動しているので、エンドポイントはhttp://localhost:4566です。

$ aws --endpoint-url http://localhost:4566 s3 mb s3://public-bucket
make_bucket: public-bucket

適当に、アップロードするファイルを用意します。

$ echo 'Hello Storage!!' > hello.txt


$ curl -LO https://tomcat.apache.org/res/images/tomcat.png


$ curl -LO https://dlcdn.apache.org/tomcat/tomcat-10/v10.0.27/bin/apache-tomcat-10.0.27.tar.gz

アップロード。

$ aws --endpoint-url http://localhost:4566 s3 cp ./hello.txt s3://public-bucket/hello.txt
upload: ./hello.txt to s3://public-bucket/hello.txt


$ aws --endpoint-url http://localhost:4566 s3 cp ./tomcat.png s3://public-bucket/images/tomcat.png
upload: ./tomcat.png to s3://public-bucket/images/tomcat.png


$ aws --endpoint-url http://localhost:4566 s3 cp ./apache-tomcat-10.0.27.tar.gz s3://public-bucket/archives/apache-tomcat-10.0.27.tar.gz
upload: ./apache-tomcat-10.0.27.tar.gz to s3://public-bucket/archives/apache-tomcat-10.0.27.tar.gz

これらのオブジェクトに対して、http://[LocalStackのアドレス]:[LocalStackのポート]/[バケット名]/[キー]でアクセスできるようです。

$ curl localhost:4566/public-bucket/hello.txt
Hello Storage!!

特になにも設定は要らなかったので驚きました…。

Content-Typeなどはどうなっているんでしょうね?

$ curl -I localhost:4566/public-bucket/hello.txt
HTTP/1.1 200
content-type: text/plain
Content-Length: 16
Server: Werkzeug/2.1.2 Python/3.10.8
Date: Thu, 26 Jan 2023 06:22:40 GMT
content-md5: W+wurJXnAgnol21+VbZXOg==
ETag: "5bec2eac95e70209e8976d7e55b6573a"
last-modified: Thu, 26 Jan 2023 06:19:38 GMT
x-amzn-requestid: 2JkUKhj1v9ZqSoMks0lPZllR58tUG2u4SHUECYD4Y0xzGYCgVL3B
Connection: close
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: HEAD,GET,PUT,POST,DELETE,OPTIONS,PATCH
Access-Control-Allow-Headers: authorization,cache-control,content-length,content-md5,content-type,etag,location,x-amz-acl,x-amz-content-sha256,x-amz-date,x-amz-request-id,x-amz-security-token,x-amz-tagging,x-amz-target,x-amz-user-agent,x-amz-version-id,x-amzn-requestid,x-localstack-target,amz-sdk-invocation-id,amz-sdk-request
Access-Control-Expose-Headers: etag,x-amz-version-id
x-amz-request-id: 5F70896B07057A95
x-amz-id-2: MzRISOwyjmnup5F70896B07057A957/JypPGXLh0OVFGcJaaO3KW/hRAqKOpIEEp
date: Thu, 26 Jan 2023 06:22:40 GMT
server: hypercorn-h11


$ curl -I localhost:4566/public-bucket/images/tomcat.png
HTTP/1.1 200
content-type: image/png
Content-Length: 8410
Server: Werkzeug/2.1.2 Python/3.10.8
Date: Thu, 26 Jan 2023 06:23:06 GMT
content-md5: QVzVnSsAbnlbn5nFW4PX7A==
ETag: "415cd59d2b006e795b9f99c55b83d7ec"
last-modified: Thu, 26 Jan 2023 06:19:45 GMT
x-amzn-requestid: sgohQAVCccKR1Dz3ZEg3oSj5stKqYBoD7rpiMF8NGWbsnMIB0l7w
Connection: close
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: HEAD,GET,PUT,POST,DELETE,OPTIONS,PATCH
Access-Control-Allow-Headers: authorization,cache-control,content-length,content-md5,content-type,etag,location,x-amz-acl,x-amz-content-sha256,x-amz-date,x-amz-request-id,x-amz-security-token,x-amz-tagging,x-amz-target,x-amz-user-agent,x-amz-version-id,x-amzn-requestid,x-localstack-target,amz-sdk-invocation-id,amz-sdk-request
Access-Control-Expose-Headers: etag,x-amz-version-id
x-amz-request-id: B96A1E2F60DCC523
x-amz-id-2: MzRISOwyjmnupB96A1E2F60DCC5237/JypPGXLh0OVFGcJaaO3KW/hRAqKOpIEEp
date: Thu, 26 Jan 2023 06:23:06 GMT
server: hypercorn-h11


$ curl -I localhost:4566/public-bucket/archives/apache-tomcat-10.0.27.tar.gz
HTTP/1.1 200
content-type: application/x-tar
Content-Length: 11984522
Server: Werkzeug/2.1.2 Python/3.10.8
Date: Thu, 26 Jan 2023 06:23:22 GMT
ETag: "b1b0a5be661bb2e3d5e29dbf9daa4db7-2"
last-modified: Thu, 26 Jan 2023 06:19:49 GMT
x-amzn-requestid: H8pQahqIcVroyKEg9v1YdlB86oYWwzBNg30Xv3jiSTlx1qCBY77w
Connection: close
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: HEAD,GET,PUT,POST,DELETE,OPTIONS,PATCH
Access-Control-Allow-Headers: authorization,cache-control,content-length,content-md5,content-type,etag,location,x-amz-acl,x-amz-content-sha256,x-amz-date,x-amz-request-id,x-amz-security-token,x-amz-tagging,x-amz-target,x-amz-user-agent,x-amz-version-id,x-amzn-requestid,x-localstack-target,amz-sdk-invocation-id,amz-sdk-request
Access-Control-Expose-Headers: etag,x-amz-version-id
x-amz-request-id: 7B8DD387A5E98F06
x-amz-id-2: MzRISOwyjmnup7B8DD387A5E98F067/JypPGXLh0OVFGcJaaO3KW/hRAqKOpIEEp
date: Thu, 26 Jan 2023 06:23:22 GMT
server: hypercorn-h11

ちゃんと設定されていそうですが、これはContent-Typeが未指定の場合にAWS CLIというかPythonが推測したもののようです。

https://github.com/aws/aws-cli/blob/2.9.18/awscli/customizations/s3/utils.py#L330-L336

mimetypes --- ファイル名を MIME 型へマップする — Python 3.9.12 ドキュメント

なるほど。

$ python3
Python 3.10.6 (main, Nov 14 2022, 16:10:14) [GCC 11.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import mimetypes
>>> mimetypes.guess_type('hoge.png')
('image/png', None)
>>> mimetypes.guess_type('hoge.tar.gz')
('application/x-tar', 'gzip')
>>> mimetypes.guess_type('hoge.txt')
('text/plain', None)

まあ、AWS CLIなどでアップロードする際に、Content-Type(必要に応じてContent-Encodingも)を設定するのが良いような気はします。

$ aws --endpoint-url http://localhost:4566 s3 cp ./hello.txt s3://public-bucket/hello.txt --content-type 'text/plain'
$ aws --endpoint-url http://localhost:4566 s3 cp ./tomcat.png s3://public-bucket/images/tomcat.png --content-type 'image/png'
$ aws --endpoint-url http://localhost:4566 s3 cp ./apache-tomcat-10.0.27.tar.gz s3://public-bucket/archives/apache-tomcat-10.0.27.tar.gz --content-encoding 'gzip' --content-type 'application/x-gzip'

これで、Content-TypeやContent-Encodingに反映されます。

$ curl -I localhost:4566/public-bucket/archives/apache-tomcat-10.0.27.tar.gz
HTTP/1.1 200
content-type: application/x-gzip
Content-Length: 11984522
Server: Werkzeug/2.1.2 Python/3.10.8
Date: Thu, 26 Jan 2023 06:24:08 GMT
content-encoding: gzip
ETag: "b1b0a5be661bb2e3d5e29dbf9daa4db7-2"
last-modified: Thu, 26 Jan 2023 06:24:03 GMT
x-amzn-requestid: xNqrIvUIPXDQMuvH2IeF6NMvPrT6kzjBVpB3qQCgtjE4OWsYOnUl
Connection: close
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: HEAD,GET,PUT,POST,DELETE,OPTIONS,PATCH
Access-Control-Allow-Headers: authorization,cache-control,content-length,content-md5,content-type,etag,location,x-amz-acl,x-amz-content-sha256,x-amz-date,x-amz-request-id,x-amz-security-token,x-amz-tagging,x-amz-target,x-amz-user-agent,x-amz-version-id,x-amzn-requestid,x-localstack-target,amz-sdk-invocation-id,amz-sdk-request
Access-Control-Expose-Headers: etag,x-amz-version-id
x-amz-request-id: 3A824132B0C81C38
x-amz-id-2: MzRISOwyjmnup3A824132B0C81C387/JypPGXLh0OVFGcJaaO3KW/hRAqKOpIEEp
date: Thu, 26 Jan 2023 06:24:08 GMT
server: hypercorn-h11
MinIO+AWS CLIで試す

次は、MinIO+AWS CLIで試してみましょう。

MinIOをローカルで起動しているので、エンドポイントはhttp://localhost:9000です。

バケット作成。

$ aws --endpoint-url http://localhost:9000 s3 mb s3://public-bucket
make_bucket: public-bucket

ファイルをアップロード。

$ aws --endpoint-url http://localhost:9000 s3 cp ./hello.txt s3://public-bucket/hello.txt --content-type 'text/plain'
$ aws --endpoint-url http://localhost:9000 s3 cp ./tomcat.png s3://public-bucket/images/tomcat.png --content-type 'image/png'
$ aws --endpoint-url http://localhost:9000 s3 cp ./apache-tomcat-10.0.27.tar.gz s3://public-bucket/archives/apache-tomcat-10.0.27.tar.gz --content-encoding 'gzip' --content-type 'application/x-gzip'

LocalStackと同じようにhttp://[MinIOのアドレス]:[MinIOのポート]/[バケット名]/[キー]でアクセスできると思いきや、アクセス拒否されます。

$ curl localhost:9000/public-bucket/hello.txt
<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>AccessDenied</Code><Message>Access Denied.</Message><Key>hello.txt</Key><BucketName>public-bucket</BucketName><Resource>/public-bucket/hello.txt</Resource><RequestId>173DC7EFA397313E</RequestId><HostId>0d54e0c2-1fda-463c-a355-f9b30c0a2c9e</HostId></Error>

ここで、以下のように作成したバケットに対して誰もアクセスできるようなポリシーを作成します。

bucket-policy.json

{
  "Version":"2012-10-17",
  "Statement":[
    {
      "Effect":"Allow",
      "Action": [
        "s3:GetBucketLocation",
        "s3:ListBucket"
      ],
      "Resource":"arn:aws:s3:::public-bucket",
      "Principal": {
        "AWS": "*"
      }
    },
    {
      "Effect":"Allow",
      "Action": "s3:GetObject",
      "Resource":"arn:aws:s3:::public-bucket/*",
      "Principal": {
        "AWS": "*"
      }
    }
  ]
}

適用。

$ aws --endpoint-url http://localhost:9000 s3api put-bucket-policy --bucket public-bucket --policy file://bucket-policy.json

これで、アクセス可能になります。

$ curl localhost:9000/public-bucket/hello.txt
Hello Storage!!
MinIO+MinIO CLIで試す

MinIOの場合は、CLIがあるのでそちらでも確認しておきましょう。

といっても、権限設定の話を中心にしておきます。

anonymous setで、認証されていないユーザーに対する振る舞いを指定できます。

mc anonymous set — MinIO Object Storage for Linux

none、download、upload、publicで簡易的にポリシーを設定でき、downloadでは誰でもダウンロードが可能になるポリシーです。
今回はdownloadを使います。

バケット作成。

$ mcli mb myminio/public-bucket
Bucket created successfully `myminio/public-bucket`.

誰からもダウンロードできるように設定。

$ mcli anonymous set download myminio/public-bucket

これで、アップロードしたファイルを誰でも参照できるようになります。
※ファイルのアップロードは省略しています

$ curl localhost:9000/public-bucket/hello.txt
Hello Storage!!

ちなみに、この時のバケットポリシーがどうなっているかというと、このようになっています。

$ aws --endpoint-url http://localhost:9000 s3api get-bucket-policy --bucket public-bucket
{
    "Policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Action\":[\"s3:GetBucketLocation\",\"s3:ListBucket\"],\"Resource\":[\"arn:aws:s3:::public-bucket\"]},{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Action\":[\"s3:GetObject\"],\"Resource\":[\"arn:aws:s3:::public-bucket/*\"]}]}"
}

文字列からJSONにして、フォーマットするとこうですね。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": [
          "*"
        ]
      },
      "Action": [
        "s3:GetBucketLocation",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::public-bucket"
      ]
    },
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": [
          "*"
        ]
      },
      "Action": [
        "s3:GetObject"
      ],
      "Resource": [
        "arn:aws:s3:::public-bucket/*"
      ]
    }
  ]
}

つまりMinIO+AWS CLIの例で適用していたバケットポリシーは、これと同じものだった、ということになります。

バケットにアップロードしたファイルを、認証情報付きでアクセスする

LocalStackはいきなり認証情報なしでアクセスできたので、MinIO限定の話になりますが。

認証情報を付与してHTTPアクセスできないものでしょうか?

バケットを作成して、ファイルをアップロード。

$ aws --endpoint-url http://localhost:9000 s3 mb s3://private-bucket
$ aws --endpoint-url http://localhost:9000 s3 cp ./hello.txt s3://private-bucket/hello.txt

この状態になるのを、バケットポリシーを変更せずになんとかしようという話です。

$ curl localhost:9000/private-bucket/hello.txt
<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>AccessDenied</Code><Message>Access Denied.</Message><Key>hello.txt</Key><BucketName>private-bucket</BucketName><Resource>/private-bucket/hello.txt</Resource><RequestId>173DC99B32B3BBD0</RequestId><HostId>11bf943b-4421-48df-ad00-a604a07470d6</HostId></Error>

どうやら、AWSのバージョン4の署名を付ければ良さそうではあります。

AWS API リクエスト署名の要素 - AWS 全般のリファレンス

また、curl 7.75で--aws-sigv4というオプションが追加されており、こちらを使えば良さそうです。

CURLOPT_AWS_SIGV4

--aws-sigv4にはaws:amz:[リージョン]:[サービス名]を指定します。aws:amzは固定です。
また、--userにはアクセスキーIDとシークレットアクセスキーをユーザー名とパスワードとして指定します。

$ curl --aws-sigv4 'aws:amz:ap-northeast-1:s3' --user 'minioadmin:minioadmin' localhost:9000/private-bucket/hello.txt
<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>SignatureDoesNotMatch</Code><Message>The request signature we calculated does not match the signature you provided. Check your key and signing method.</Message><Key>hello.txt</Key><BucketName>private-bucket</BucketName><Resource>/private-bucket/hello.txt</Resource><RequestId>173DCABFE9F561A4</RequestId><HostId>11bf943b-4421-48df-ad00-a604a07470d6</HostId></Error>

なのですが、うまくいきません…。

どうやら80または443以外のポートがエンドポイントになっていると、動かないようです。

--aws-sigv4 does not sign requests with remote port (not 80 or 443) correctly. · Issue #9550 · curl/curl · GitHub

現時点でのワークアラウンドとしては、Hostヘッダーからポートを外すと動作します。

$ curl --aws-sigv4 'aws:amz:ap-northeast-1:s3' --user 'minioadmin:minioadmin' -H 'Host: localhost' localhost:9000/private-bucket/hello.txt
Hello Storage!!

curl 7.86で修正されているようなのですが、

aws_sigv4: fix headers computation for some unhandled cases. by outscale-mgo · Pull Request #7966 · curl/curl · GitHub

手元のcurlは7.81だったので…。

$ curl --version
curl 7.81.0 (x86_64-pc-linux-gnu) libcurl/7.81.0 OpenSSL/3.0.2 zlib/1.2.11 brotli/1.0.9 zstd/1.4.8 libidn2/2.3.2 libpsl/0.21.0 (+libidn2/2.3.2) libssh/0.9.6/openssl/zlib nghttp2/1.43.0 librtmp/2.3 OpenLDAP/2.5.13
Release-Date: 2022-01-05
Protocols: dict file ftp ftps gopher gophers http https imap imaps ldap ldaps mqtt pop3 pop3s rtmp rtsp scp sftp smb smbs smtp smtps telnet tftp
Features: alt-svc AsynchDNS brotli GSS-API HSTS HTTP2 HTTPS-proxy IDN IPv6 Kerberos Largefile libz NTLM NTLM_WB PSL SPNEGO SSL TLS-SRP UnixSockets zstd

Docker Hubのcurlのイメージから、新しいcurlで試すとうまくいきました。

$ docker container run -it --rm --name curl curlimages/curl:7.87.0 curl --aws-sigv4 'aws:amz:ap-northeast-1:s3' --user 'minioadmin:minioadmin' [ホストのIPアドレス]:9000/private-bucket/hello.txt
Hello Storage!!

バージョン。

$ docker container run -it --rm --name curl curlimages/curl:7.87.0 curl --version
curl 7.87.0-DEV (x86_64-pc-linux-musl) libcurl/7.87.0-DEV OpenSSL/1.1.1s zlib/1.2.12 brotli/1.0.9 libssh2/1.10.0 nghttp2/1.47.0
Release-Date: [unreleased]
Protocols: dict file ftp ftps gopher gophers http https imap imaps mqtt pop3 pop3s rtsp scp sftp smb smbs smtp smtps telnet tftp
Features: alt-svc AsynchDNS brotli HSTS HTTP2 HTTPS-proxy IPv6 Largefile libz NTLM NTLM_WB SSL threadsafe TLS-SRP UnixSockets

こんなところでしょうか。

まとめ

LocalStackとMinIOで、バケットにアップロードしたファイルをHTTPで参照してみました。

LocalStackはなにも考えずにアクセスできましたが、MinIOはポリシーの設定が必要にはなるものの、期待した使い方はできるということが
わかって良かったかなと思います。

それから、curlのAWSのバージョン4の署名に関するオプションは知りませんでした…。