環境
Serverless Framework: 2.14.0
構成
以下のような構成にします。特定のパスへのアクセスがあったら署名付きURLをLambdaで取得し、それを用いて直接S3にアクセスするようにします。
今回は色々簡略化していますが、もし実運用を考えられる場合はcognitoでの認証等も検討する必要があると思います。
実装
Serverless Frameworkを導入して、設定を記述していきます。
serverless create -t aws-go -p go-api-lambda-s3
go.modを作成して、必要なパッケージを導入します。
cd go-api-lambda-s3
go mod init go-api-lambda-s3
go get github.com/aws/aws-lambda-go
go get github.com/aws/aws-sdk-go
最終的なディレクトリ構成は以下のようにします。
.
├── Makefile
├── bin
│ └── download
│ └── upload
├── config
│ └── setting.json
├── download
│ └── main.go
├── go.mod
├── go.sum
├── serverless.yml
└── upload
└── main.go
setting.json
にはバケット名を記載しておきます。
{
"BUCKET_NAME": "hogehoge"
}
S3へのアップロード
serverless.yml
は以下のように記述します。権限の設定、バケットの作成と、/upload
に来た時に起動するLambdaの指定を行っています。
service: go-api-lambda-s3
provider:
name: aws
runtime: go1.x
region: ap-northeast-1
iamRoleStatements:
- Effect: "Allow"
Action:
- "s3:GetObject"
- "s3:PutObject"
Resource:
Fn::Join:
- ""
- - "arn:aws:s3:::"
- ${self:provider.environment.Bucket}
- "/*"
environment:
Bucket: ${file(config/setting.json):BUCKET_NAME}
package:
exclude:
- ./**
include:
- ./bin/**
functions:
upload:
handler: bin/upload
environment: ${file(./config/setting.json)}
events:
- http:
path: upload
method: get
resources:
Resources:
Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: ${self:provider.environment.Bucket}
upload/main.go
です。5分間だけ署名付きURLが有効になるように設定しています。
また、エラー処理は省略していますのでご了承ください。
package main
import (
"net/url"
"os"
"time"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
)
// Response ...
type Response events.APIGatewayProxyResponse
// Request ...
type Request events.APIGatewayProxyRequest
// Handler ...
func Handler(request Request) (Response, error) {
sess := session.Must(session.NewSession(&aws.Config{
Region: aws.String("ap-northeast-1")},
))
svc := s3.New(sess)
fileKey, _ := url.PathUnescape(request.QueryStringParameters["fileKey"])
resp, _ := svc.PutObjectRequest(&s3.PutObjectInput{
Bucket: aws.String(os.Getenv("BUCKET_NAME")),
Key: aws.String(fileKey),
})
url, _ := resp.Presign(5 * time.Minute)
res := Response{
StatusCode: 200,
Headers: map[string]string{
"Content-Type": "text/html; charset=utf-8",
},
Body: url,
}
return res, nil
}
func main() {
lambda.Start(Handler)
}
実際にデプロイを行った後、テスト用のファイルを作って確認してみます。
$ echo "test" > test.txt
$ cat test.txt
test
$ url=$(curl https://xxxxx.ap-northeast-1.amazonaws.com/dev/upload --get --data-urlencode 'fileKey=test.txt')
$ curl -X PUT --upload-file test.txt $url
実際にS3のコンソールを確認してみると、アップロードされていることが確認できます。
また、しばらく時間が経過してから同様のURLにアクセスしてみるとちゃんとアクセスが拒否されました。
$ curl -X PUT --upload-file test.txt $url
<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>AccessDenied</Code><Message>Request has expired</Message><X-Amz-Expires>300</X-Amz-Expires><Expires>2020-12-13T10:49:55Z</Expires><ServerTime>2020-12-13T10:54:01Z</ServerTime><RequestId>HOGE</RequestId><HostId>FUGA</HostId></Error>
S3からのダウンロード
serverless.yml
にダウンロード用の署名付きURLを取得するための設定を追記します。
functions:
upload:
handler: bin/upload
environment: ${file(./config/setting.json)}
events:
- http:
path: upload
method: get
download:
handler: bin/download
environment: ${file(./config/setting.json)}
events:
- http:
path: download
method: get
download/main.go
を追加しますが、アップロード用の署名付きURLを取得するためのコードとほぼ変わらないです。 PutObjectRequest
となっていた部分だけ以下のように修正します。
resp, _ := svc.GetObjectRequest(&s3.GetObjectInput{
Bucket: aws.String(os.Getenv("BUCKET_NAME")),
Key: aws.String(fileKey),
})
再度デプロイした後にアクセスして確認してみます。ファイルの閲覧もダウンロードも問題ないですね。
$ url=$(curl https://xxxx.ap-northeast-1.amazonaws.com/dev/download --get --data-urlencode 'fileKey=test.txt')
$ curl $url
test
$ curl -o test_download.txt -X GET $url
$ cat test_download.txt
test
またしばらく時間が経過してから試してみますと、閲覧できなくなっています。
$ curl $url
<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>AccessDenied</Code><Message>Request has expired</Message><X-Amz-Expires>300</X-Amz-Expires><Expires>2020-12-13T11:04:22Z</Expires><ServerTime>2020-12-13T11:13:19Z</ServerTime><RequestId>HOGE</RequestId><HostId>FUGA</HostId></Error>
まとめ
クライアントサイドから直接S3へアクセスするのをサーバーレスで試してみようと思い、やってみました。 Serverless FrameworkやGoは最近勉強を進めているのですが、色々便利で楽しいですね。まだまだ精進したいと思います。