Works by

Ren's blog

@rennnosuke_rk 技術ブログです

【Go】S3互換local storageとしてMinIOを立ち上げてaws-sdk-go-v2から接続する

MinIOドキュメントに aws-sdk-go を使用したサンプルはあるのですが、 aws-sdk-go-v2 のものはないため備忘録を残しておきます。

MinIO

オープンソースのオブジェクトストレージです。 S3互換のため、S3 API経由で接続することができます。

min.io

MinIO の立ち上げ

Docker imageがあるのでこれを利用してserver用コンテナを立ち上げます。

$ docker container run -d --name minio -p 9000:9000 -p 9001:9001 minio/minio server /data --console-address ":9001"

MinIOには管理コンソールがあるので、アプリケーション用だけでなく管理コンソール用portも指定&forwardingします。

管理コンソールのportは --console-address で指定できます。指定しないとephemeral portが毎回ランダムに割り当てられてしまうので、明示的に指定したほうが楽です。

管理画面はブラウザ上でアクセスでき、credentialは↓で確認できます。credentialは環境変数 MINIO_ROOT_USER MINIO_ROOT_PASSWORD で設定できますが、特に指定がなければ minioadmin:minioadmin になります。

$ docker logs minio 
API: http://172.17.0.2:9000  http://127.0.0.1:9000 

Console: http://172.17.0.2:9001 http://127.0.0.1:9001 

Documentation: https://docs.min.io
WARNING: Detected default credentials 'minioadmin:minioadmin', we recommend that you change these values with 'MINIO_ROOT_USER' and 'MINIO_ROOT_PASSWORD' environment variables

aws-sdk-go-v2でMinIOを叩く

import (
    "bytes"
    "context"
    "log"

    "github.com/aws/aws-sdk-go-v2/aws"
    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/aws/aws-sdk-go-v2/credentials"
    "github.com/aws/aws-sdk-go-v2/service/s3"
)

func main() {
    ctx := context.Background()

    accessKey := "<access-key>"
    secretKey := "<secret-key>"
    cred := credentials.NewStaticCredentialsProvider(accessKey, secretKey, "")

    endpoint := aws.EndpointResolverFunc(func(service, region string) (aws.Endpoint, error) {
        return aws.Endpoint{
            URL: "http://localhost:9000",
        }, nil
    })

    cfg, err := config.LoadDefaultConfig(ctx, config.WithCredentialsProvider(cred), config.WithEndpointResolver(endpoint))
    if err != nil {
        log.Fatalln(err)
    }

    // change object address style
    client := s3.NewFromConfig(cfg, func(options *s3.Options) {
        options.UsePathStyle = true
    })

    // get buckets
    lbo, err := client.ListBuckets(ctx, nil)
    if err != nil {
        log.Fatalln(err)
    }
    buckets := make(map[string]struct{}, len(lbo.Buckets))
    for _, b := range lbo.Buckets {
        buckets[*b.Name] = struct{}{}
    }

    // create 'develop' bucket if not exist
    bucketName := "develop"
    if _, ok := buckets[bucketName]; !ok {
        _, err = client.CreateBucket(ctx, &s3.CreateBucketInput{
            Bucket: &bucketName,
        })
        if err != nil {
            log.Fatalln(err)
        }
    }

    // put object
    _, err = client.PutObject(ctx, &s3.PutObjectInput{
        Bucket: &bucketName,
        Key:    aws.String("hogehoge"),
        Body:   bytes.NewReader([]byte("Hello, MinIO!")),
    })
    if err != nil {
        log.Fatalln(err)
    }
}

aws-sdk-go-v2からMinIOを叩く際に考慮する点が1つあります。

オブジェクトアドレスをpath styleにする

S3にはpath style と virtual-hosted styleのアドレスがあります。path styleではバケット名がpathに含まれていましたが、virtual-hosted styleではURLドメインに含まれるようになります。

  • path style: https://s3-us-east-1.amazonaws.com/bucket-name/images/obj.jpeg
  • virtual-hosted style: https://bucket-name.s3.amazonaws.com/images/obj.jpeg

AWS S3だと現時点でvirtual-hosted style のアドレスが使用されていますが、MinIOは対応していないのでpath styleでアクセスする必要があります。

path styleへの変更は s3.NewFromConfigoptions.UsePathStyle=true 書き換えを行う func(options *s3.Options) を渡します。

client := s3.NewFromConfig(cfg, func(options *s3.Options) {
    options.UsePathStyle = true
})

管理コンソール上でuploadされたオブジェクトを確認できます。

f:id:rennnosukesann:20211122111122p:plain

参考文献

hub.docker.com

github.com

docs.min.io

github.com

【Go】aws-sdk-go-v2でio.Seeker未実装streamを使用してS3 objectをuploadする

aws-sdk-go-v2 でS3にオブジェクトをアップロードするには PutObject が利用できます。 引数となる PutObjectInputBody fieldに、アップロードしたいオブジェクトコンテンツを io.Reader で渡すことができます。 例えば、下記例では bytes.Buffer 型の値を渡しています。

input := &s3.PutObjectInput{
    Bucket:        aws.String(bucketName),
    Key:           aws.String(key),
    Body:          bytes.NewBuffer(b) // b : object binary
}

resp, err := client.PutObject(ctx, input)
if err != nil {
    return err
}

ただしアップロードは失敗し、下記のようなerrorが返ってきます。

operation error S3: PutObject, failed to compute payload hash: failed to seek body to start, request stream is not seekable

error文言からは PutObjectペイロードのハッシュを計算するため、 Bodyio.Seeker を実装したstreamを渡さなければいけないことがわかります。 bytes.BufferSeek(offset int64, whence int) (int64, error) を実装していないので失敗します。

// Seeker is the interface that wraps the basic Seek method.
//
// Seek sets the offset for the next Read or Write to offset,
// interpreted according to whence:
// SeekStart means relative to the start of the file,
// SeekCurrent means relative to the current offset, and
// SeekEnd means relative to the end.
// Seek returns the new offset relative to the start of the
// file and an error, if any.
//
// Seeking to an offset before the start of the file is an error.
// Seeking to any positive offset is legal, but the behavior of subsequent
// I/O operations on the underlying object is implementation-dependent.
type Seeker interface {
    Seek(offset int64, whence int) (int64, error)
}

このことは公式ドキュメントでも言及されています。

aws.github.io

os.File など io.Seeker 実装型の値で渡せば問題ないのですが、 他の io.Reader で渡したい場合、明示的にコンテンツサイズを計算して渡してあげる必要があります。 具体的には

  1. PutObjectInput に、ContentLength を設定する
  2. PutObject 第三実引数に、 SwapComputePayloadSHA256ForUnsignedPayloadMiddleware を渡した WithAPIOptions() 呼び出しを設定する

とします。

input := s3.PutObjectInput{
    Bucket:        aws.String(bucketName),
    Key:           aws.String(key),
    Body:          bytes.NewBuffer(b),
    ContentLength: int64(len(b)), // 1.
}

resp, err := client.PutObject(ctx, input, s3.WithAPIOptions(
    v4.SwapComputePayloadSHA256ForUnsignedPayloadMiddleware, // 2.
))
if err != nil {
    return err
}

または、下記のようにUpload Managerを使用するとペイロードハッシュのことを考慮せずとも io.Reader を渡せます。 ただしこちらの方法を使用すると、PutObject APIではなくマルチパートアップロードが実行されるので注意です。

uploader := manager.NewUploader(client)
_, err = uploader.Upload(ctx, &s3.PutObjectInput{
    Bucket: aws.String(bucketName),
    Key:    aws.String(key),
    Body:   bytes.NewBuffer(b),
})

aws.github.io

module version

github.com/aws/aws-sdk-go-v2 v1.11.1
github.com/aws/aws-sdk-go-v2/config v1.10.2
github.com/aws/aws-sdk-go-v2/credentials v1.6.2
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.7.2
github.com/aws/aws-sdk-go-v2/service/s3 v1.19.1

参考文献

aws.github.io

docs.aws.amazon.com

【Golang】io.Pipeのr/wブロック

io.Pipe

io パッケージの関数 io.Pipe()io.Writer を実装した PipeReaderio.Reader を実装した PipeWriter ポインタを返します。 PipeWriter.Write() で書き込みを行うと、その内容を PipeReader.Read() で読みこむことができます。

pr, pw := io.Pipe()

go func(w io.Writer) {
    s := []byte("string")
    if _, err := w.Write(s); err != nil {
        t.Error(err)
    }
}(pw)

b := make([]byte, 1024)
if _, err := pr.Read(b); err != nil {
    t.Error(err)
}
fmt.Println(b) // -> [115 116 114 105 110 103 0 ... ]

PipeReader PipeWriter 共々内部的には同じ pipe 型ポインタを持っていて、これを介してコンテンツを読み書きしています。

pipe.go
// A PipeReader is the read half of a pipe.
type PipeReader struct {
    p *pipe
}
...
// A PipeWriter is the write half of a pipe.
type PipeWriter struct {
    p *pipe
}

io.Pipe のr/wブロック

PipeReader.Read() PipeWriter.Write() は互いの操作をブロックします。すなわち PipeReader.Read()PipeWriter.Write() が呼ばれるまでブロックされ、 PipeWriter.Write() もまた PipeReader.Read() が呼ばれるまでブロックされます。そのため上記の例ではgoroutineで非同期に PipeWriter.Write() を呼び、 PipeReader.Read() がブロックされることを防いでいます。

ではgoroutineを使用せず同期的に PipeReader.Read() あるいは PipeWriter.Write() を呼び出すとそのままプログラムの実行がstopするのか?と思いきやfatal errorで終了します。

pr, pw := io.Pipe()

s := []byte("string")
if _, err := pw.Write(s); err != nil {
    panic(err)
}

b := make([]byte, 1024)
if _, err := pr.Read(b); err != nil {
    panic(err)
}
fmt.Println(b)
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [select]:
io.(*pipe).Write(0xc0000201e0, 0xc00001c09a, 0x6, 0x6, 0x0, 0x0, 0x0)
        /usr/local/go/src/io/pipe.go:94 +0x1e5
io.(*PipeWriter).Write(...)
        /usr/local/go/src/io/pipe.go:163

これは pipe が内部で保持するチャネルが入力待ちの状態に陥り、main goroutine含む全てのgoroutineがstopしてしまうために発生します。

pipe.go
func (p *pipe) Write(b []byte) (n int, err error) {
    select {
    case <-p.done:
        return 0, p.writeCloseError()
    default:
        p.wrMu.Lock()
        defer p.wrMu.Unlock()
    }

    for once := true; once || len(b) > 0; once = false {
        select {
        case p.wrCh <- b: // ここでp.wrChが出力可能になるまでブロック
            nw := <-p.rdCh
            b = b[nw:]
            n += nw
        case <-p.done:
            return n, p.writeCloseError()
        }
    }
    return n, nil
}

...

func (p *pipe) Read(b []byte) (n int, err error) {
    select {
    case <-p.done:
        return 0, p.readCloseError()
    default:
    }

    select {
    case bw := <-p.wrCh: // ここで入力可能になるまでブロック
        nr := copy(b, bw)
        p.rdCh <- nr
        return nr, nil
    case <-p.done:
        return 0, p.readCloseError()
    }
}

PipeReader.Read() PipeWriter.Write() がブロックされるのは、内部のチャネルのブロックによるものでした。 こうして見ると、 io.Pipe 関数はチャネルによるgoroutine間の []byte I/Oを抽象化するWrapperのようにも感じます。

nits

中間にバッファを介せば、一応同期的にr/w処理を分けることができます。バッファ分のメモリ容量を必要としますが...

buf := bytes.Buffer{}

s := []byte("string")
if _, err := buf.Write(s); err != nil {
    t.Error(err)
}

b := make([]byte, 1024)
if _, err := buf.Read(b); err != nil {
    t.Error(err)
}
fmt.Println(b)

参考文献

golang.org

【Golang】Unicode上複数コードからなる文字をruneで扱う場合の挙動

検証環境

Mac OS Catalina 10.15.6
Go1.15.6

tl;dr

Unicode上でn個のコードからなる1文字を []rune に変換すると、[]rune スライス長は n になります。

前置き

Goにおける文字列型 string の値は、 []byte スライスの値としても扱うことができます。 例えば以下のように string[]byte にキャスト可能な他、

s := "こんにちは、Golang"
b := []byte(s) // []byte配列用にmemory allocate され、バイト列もcopyされる

string の値に要素アクセスすると対応する位置の byte 値を取得します。

s := "こんにちは、Golang"
b := s[0] // 227

文字列の長さを len 関数で取得すると、 []byte スライスとしての長さが返ってきます。

s := "こんにちは、Golang"
length := len(s) // 24

しかし実際には、本来の文字列一文字一文字についてアクセスしたい場合が多いと思います。(例えば こんにちは、Golang を12文字の「意味のある文字の配列」として扱いたい、など) Golangでは rune 型を使用し、この問題を解消します。

rune

文字列を []byte スライスとして扱う場合、それは元の各文字に割り当てられたUnicode(ISO 10646)をそれぞれUTF-8 encodingに従いバイト列に変換したものになります。

s := "こんにちは、Golang"
b := s[0]
fmt.Println(b)  // -> 227 
fmt.Printf("%v\n", []byte("こ")) // -> [227 129 147]
fmt.Printf("%+q\n", "こ") // -> "\u3053" (Unicode)
fmt.Printf("%q\n", "こ") // -> "12371" (Unicode code point)

Unicodeの単位で文字列の各文字にアクセスするには、文字列を []rune スライスにキャストします。

s := "こんにちは、Golang"
r := []rune(s)
fmt.Println(r[0]) // -> 12371
fmt.Println(string(r[0])) // -> こ

あるいは for で文字列に対して rangeによる要素アクセスを実行すると、文字列要素に rune 単位でアクセスすることができます。

s := "こんにちは、Golang"
for _, r := range s {
    fmt.Print(string(r))
} 
// -> こんにちは、Golang

複数コードからなる文字をruneとして扱う場合

先程「Unicodeの単位」と述べましたが、Unicodeには複数のcodeで一つの文字を表現するパターンもあります。 tech.sanwasystem.com

例えば、日本語の漢字のうち微妙に表現が異なる漢字郡は「異体字セレクタ」として扱われます。異体字セレクタでは同じ意味の似た文字に対して同じコードを与え、さらにそれらを識別するためのコードも与えることで(計2つ)、一意な文字を表現します。

ja.wikipedia.org

以下の例は2つのcodeからなる漢字 邊󠄆( U+908A U+E0106 )を []rune に変換したものです。この漢字、右上の「自」が「白」になっているパターンの「わた」なんですがブログ上の表記ではわからないかもです(上記文献を参照)。

wata := "邊󠄄"
rWata := []rune(wata)
fmt.Printf("%+q\n", rWata) // -> ['\u908a' '\U000e0104']
fmt.Println(len(rWata)) // -> 2

出力例のように、異体字セレクタ文字列を []rune に変換すると。長さ2となることがわかります。

fmt.Println(rWata[0]) // 37002
fmt.Println(string(rWata[0])) // -> 邊󠄄
fmt.Println(string(rWata[1])) // -> (空文字)
fmt.Printf("%+q\n",rWata[0]) // -> '\u908a'

先頭の文字を出力すると、ベースとなる文字が出現します。

このように文字列を rune で扱う場合も、Unicode上で複数codeからなる文字を扱う場合には想定する文字列長と異なってしまうので注意が必要です。

参考文献

blog.golang.org

tech.sanwasystem.com

ja.wikipedia.org

自前のブログからはてなブログに戻しました

あけましておめでとうございます。

2020年初めにブログを(半)自前運用1に移行したのですが、諸々面倒だったのでブログをはてなに戻すことにしました。
自前運用ブログの記事もこちらに移しました。

rennnosukesann.hatenablog.com

ついでにデザインテーマも変更しました。

自前運用のブログで面倒だったところ

以下面倒だった点です。
ちなみにブログはHugoでブログテンプレートを作り、Netlifyにホスティングする形式をとっていました。

画像挿入をMarkdownに記述+指定ディレクトリに置かなければいけない

Hugoにはブログ記事を編集するGUIもありましたが、はてブのほうがストレスが少なかったです。

プレビューが実際の内容と一致しない

エディタでmarkdownプレビュー見ても実際どう描画されるか不明なため、結局何回もdeployしてました。
ローカルで記事見るにもテンプレート使ったbuildが必要でした。

デプロイがたまにコケる

これはしょうがないです。Netlifyも無償枠だったので。。。

他マシン環境で執筆環境を作るのが少し面倒

ブログ用のリポジトリをクローンして、書いて、pushして・・・と、ちょっと面倒でした。

といったところです。見事にSaaSを使わないことによるデメリットを享受しています。

運用する前に上記面倒のいくつかは想定はしていたし、当初は自前ブログやってみるぞ意欲があったのでそのノリで初めてはみたのですが、思ったよりしんどいなと思ったので戻そうと思いました。

HugoとNetlify自体は良いツール・サービスでした。Hugoはブログテンプレートを多くの有志が作成した中から無料で使えるし、Netlifyは無償でホストを提供してくれる上にGithubと連携して勝手にCIしてくれます。それでも自前運用自体に上位のような手間があったので、やっぱりブログを細かくカスタマイズしたり、フロントを完全に自前で作りたい人などが自前運用に適しているのかもしれません。

今後

幸いにもはてなの方の拙ブログ(?)を今も読んでくださっている方がいらっしゃるようで、平日の多いときには400viewくらいにはなっています。

f:id:rennnosukesann:20210102134555p:plain

一年も放置したんだから流石にがっつり減るだろうと思っていたのですが、思ったよりview数の減りが少なくてびっくりしました(100くらいになると思ってた)。
ちなみに2年前に毎日ブログ更新していたときは平日概ね500-600viewくらいでした。

rennnosukesann.hatenablog.com

自分はブログをあくまで自分の学びのきっかけや備忘録のために書く多いのですが、それでも多くの人の役に立つような記事を残せたらなあと思う次第です。

ということで2021年もポツポツと記事を書いていくと思いますが、今年もどうぞよろしくお願いします。


  1. 結局ブログの格子はHTMLジェネレータだし、サーバー管理はホスティングサービスにまかっせきりなので..

【OpenAPI】PrismでOpenAPIドキュメントからモックサーバーを起動する

Prism

Stoplight 社が提供する1OpenAPI ドキュメントからモックサーバーを起動するツール。format は OpenAPI2/3 をサポートする。

Usage

Install

Prism は node 上で実行されるため、node 実行環境を用意してインストールする。

Installation

Docker

node サーバーコンテナを立ててその中にインストールする例( 一応 Prism コンテナイメージをそのまま起動することはできる

FROM node:14.14

WORKDIR /openapi

COPY ./openapi.yml .

# install stoplight/prism
RUN npm install -g @stoplight/prism-cli

# exec - specify host as 0.0.0.0 to connect from docker host.
CMD ["prism", "mock", "openapi.yml", "--host", "0.0.0.0"]

Prism インストール後、 prism mock [openapi doc] でモックサーバーを起動する。

OpenAPI ドキュメント上で定義されたパスのうち、Path Parameter の指定はランダムに変化する。

$ docker build -t rennnosuke/prism-test .
$ docker run -it -p 4010:4010 rennnosuke/prism-test
[10:57:16 AM] › [CLI] …  awaiting  Starting Prism…
[10:57:16 AM] › [CLI] ℹ  info      POST       http://0.0.0.0:4010/pet
[10:57:16 AM] › [CLI] ℹ  info      PUT        http://0.0.0.0:4010/pet
[10:57:16 AM] › [CLI] ℹ  info      GET        http://0.0.0.0:4010/pet/findByStatus?status=pending,sold,available,available,sold,sold,pending,sold,pending,available,sold,pending,sold,available,available,available,pending,sold,available,pending
[10:57:16 AM] › [CLI] ℹ  info      GET        http://0.0.0.0:4010/pet/findByTags?tags=doloribus,consequuntur,expedita,tempora,temporibus,nemo,adipisci,molestiae,reprehenderit,voluptatibus,laboriosam,sed,pariatur,culpa,dicta,illum,qui,repellat,adipisci,incidunt
[10:57:16 AM] › [CLI] ℹ  info      GET        http://0.0.0.0:4010/pet/762
[10:57:16 AM] › [CLI] ℹ  info      POST       http://0.0.0.0:4010/pet/447
[10:57:16 AM] › [CLI] ℹ  info      DELETE     http://0.0.0.0:4010/pet/951
[10:57:16 AM] › [CLI] ℹ  info      POST       http://0.0.0.0:4010/pet/576/uploadImage
[10:57:16 AM] › [CLI] ℹ  info      GET        http://0.0.0.0:4010/store/inventory
[10:57:16 AM] › [CLI] ℹ  info      POST       http://0.0.0.0:4010/store/order
[10:57:16 AM] › [CLI] ℹ  info      GET        http://0.0.0.0:4010/store/order/4
[10:57:16 AM] › [CLI] ℹ  info      DELETE     http://0.0.0.0:4010/store/order/141
[10:57:16 AM] › [CLI] ℹ  info      POST       http://0.0.0.0:4010/user
[10:57:16 AM] › [CLI] ℹ  info      POST       http://0.0.0.0:4010/user/createWithArray
[10:57:16 AM] › [CLI] ℹ  info      POST       http://0.0.0.0:4010/user/createWithList
[10:57:16 AM] › [CLI] ℹ  info      GET        http://0.0.0.0:4010/user/login?username=non&password=provident
[10:57:16 AM] › [CLI] ℹ  info      GET        http://0.0.0.0:4010/user/logout
[10:57:16 AM] › [CLI] ℹ  info      GET        http://0.0.0.0:4010/user/placeat
[10:57:16 AM] › [CLI] ℹ  info      PUT        http://0.0.0.0:4010/user/qui
[10:57:16 AM] › [CLI] ℹ  info      DELETE     http://0.0.0.0:4010/user/ex
[10:57:16 AM] › [CLI] ▶  start     Prism is listening on http://0.0.0.0:4010
$ curl -XGET -s -D "/dev/stderr" -H "Content-type: application/json" http://0.0.0.0:4010/store/inventory
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: *
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: *
Content-type: application/json
Content-Length: 29
Date: Sun, 18 Oct 2020 11:01:52 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"property1":0,"property2":0}

tips

動的レスポンス生成

Prism ではレスポンスの値を動的に生成できる。

x-faker プロパティを Schema プロパティ上に定義することで、レスポンス中の値がランダム生成されるようになる。

指定方法は Faker.js に従う。

公式より引用)

Pet:
  type: object
  properties:
    id:
      type: integer
      format: int64
    name:
      type: string
      x-faker: name.firstName
      example: doggie
    photoUrls:
      type: array
      items:
        type: string
        x-faker: image.imageUrl
$ curl http://127.0.0.1:4010/pets/123 -H "Prefer: dynamic=true"
{
  "id": 12608726,
  "name": "Addison",
  "photoUrls": [
    "http://lorempixel.com/640/480",
    "http://lorempixel.com/640/480",
    "http://lorempixel.com/640/480",
    "http://lorempixel.com/640/480"
  ]
}

Definition Engine

Prism モックサーバーがリクエストを受け取ったときのレスポンス決定ルール。 Mock をより精密に動作させるために、また OpenAPI ドキュメントの項目を充実するために使える。

参考文献


  1. OpenAPI ドキュメントの編集ツールstoplight studioも便利

【AWS】IAMユーザーにAWS上のリソースに対するSSL/TLS通信を強制する

メモ。

特定のユーサーに AWS 上のリソースへの操作権限を与えたいが、操作のためのリクエストは SSL/TLS 通信に限定したい場合がある。例えば、S3 への API 経由のファイルアップロードなどをユーザーに許可する際、それを HTTPS に限定したいときなどがある。

SSL/TLS 通信制約は、IAM ポリシーに下記のような条件 Condition を追加することで達成できる。

{
  "Version": "2012-10-17",
  "Statement": {
    "Effect": "Allow",
    "Action": "iam:*AccessKey*",
    "Resource": "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:user/*",
    "Condition": {
      "Bool": {
        "aws:SecureTransport": "true"
      }
    }
  }
}

Condition.Bool["aws:SecureTransport"] プロパティを true にすると、ユーザーからの暗号化されていないプレーンな HTTP によるアクセスが拒否される。

参考文献

IAM ポリシーエレメント: 変数およびタグ - AWS Identity and Access Management