Go×LambdaでS3にファイルをアップロードする時に気をつけたいこと

Yu Kato
7 min readJun 6, 2019

どうも。カトウです。実務で、Go×Lambdaを使う機会があって(また会ったなGopher!)、大苦戦をしたのでその時のことについて記事にしてみます。

恵比寿の豚骨ラーメン 暖暮↑ ウママママママ🌶🌶🌶

まずやりたかったことのイメージ図↓

API GateWayにmultipart/form-dataの形式でファイルとテキストデータが送られてくる。

それを、Lambdaで取得して、S3にアップロードするという流れ。

API GateWayとLambdaは触ったことないけど、フローは明確だし、Goは半年ぐらい触ってたからなんとかなるだろうと思っていた。

そうこの時は、気づいていなかったんだ。

multipart/form-data形式の恐ろしさに。。。

API GateWayで受けとったデータをLambdaに投げる際に、下記のようなマッピングテンプレートを使う。

#set($allParams = $input.params())
{
"body-json" : $input.json('$'),
"params" : {
#foreach($type in $allParams.keySet())
#set($params = $allParams.get($type))
"$type" : {
#foreach($paramName in $params.keySet())
"$paramName" : "$util.escapeJavaScript($params.get($paramName))"
#if($foreach.hasNext),#end
#end
}
#if($foreach.hasNext),#end
#end
},
.....

Lambda側では、”body-json”を含んだGoの構造体にパースする。

type Request struct {
Body string `json:"body-json"`
}

試しに、multiple/form-datanのbodyにhoge.pngとテキストデータ “hoge”を入れてテストを走らせて、Request.Bodyの値を出力させてみた。

(諸事情により、メールの中身に出力させてますがご了承ください。SESを使ってるだけなのです)。

結果

base64にencodeされてんな!

というわけで、base64でdecodeをして出力させてみます。

おお!base64でencodeできた!

つまり、ここから、テキストデータを取得して、ファイルはosパッケージ使って、encodeした内容をfile.Write(data)みたいな形にしてS3に送れば良い!!

と言いたいところですが、まだ問題があります。

それは、このメールの画像の中から欲しい文字列とファイルのバイナリデータを正しく取得しなければならないのです。

いや、せっかくmultipart/form-dataのBodyではkey-valueの形になっていたのに、Goの正規表現とか文字列分割とかして取得しなけれならない。。

なぜそんなことになったかというと、マッピングテンプレートの

"body-json" : $input.json('$'),

$input.json('$')の値が既にkey-valueで取得することができないから。

つまり、multipart/form-dataのBodyの値を扱うには、自分で文字列をいじらなければならないのだ。

[application/jsonを使う]

そんなのは嫌なので、multipart/form-data→application/jsonに変更する。

そして、画像ファイルは中身が約1.3倍になるがbase64でエンコードしてjsonのなかに入れてAPI GateWayに送信する。

Goのハンドラの関数の引数を下の構造体、Requestにする。

type Information struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
File string `json:"file"`
}
type Request struct {
Body Information `json:"body-json"`
}

application/jsonのkeyを構造体Informationに書くことで、パースがうまくいくようになった。

次に、S3に画像をアップロードしてみる。

[Lambdaの良いところ]

Lambda関数に与えられたロールを使用するのでCredential情報をコードに書かなくても良い!

それでは、Goのコードをみてみよう!

data, _ := base64.StdEncoding.DecodeString(r.Body.Resume)
f, _ := os.Create("hoge.png")
_, err := f.Write(data)
if err != nil {
log.Println(err)
}
sess := session.Must(session.NewSession(&aws.Config{
Region: aws.String("ap-northeast-1"),
}))
uploader := s3manager.NewUploader(sess)
result, err := uploader.Upload(&s3manager.UploadInput{
ACL: aws.String("public-read"),
Bucket: aws.String("test-bucket"),
Key: aws.String("hoge.png"),
Body: f,
})
if err != nil {
log.Printf(err)
}

すると、err := uploader.Upload()でこんなエラーが出る。

BodyHashError: failed to compute body hashes

Bodyのデータがhash化できないというエラーでたので、おそらくBodyの中身が悪い。

結論からいうと、エラーの原因はos.Create()が使えないことだった。

よくわからんが、Lambdaでは、そもそもzipファイルでGoのコードを渡さないといけないなど制約が多いので、Goのファイルが置かれたディレクトリにファイルを作成するなどはできないっぽい。

なので、os.Create()を使わずにbytes.NewReader()を使い、実装を行う。

result, err := uploader.Upload(&s3manager.UploadInput{
ACL: aws.String("public-read"),
Bucket: aws.String("test-bucket"),
Key: aws.String("hoge.png"),
Body: bytes.NewReader(data),
})

これで、S3に画像をアップロードすることができた!!!!

--

--

Yu Kato

Japanese web developer/1998’s/ i wiill write about my project(Go/React) https://github.com/yutify