Fusic Tech Blog

Fusion of Society, IT and Culture

Go+serverlessで署名付きURLの取得
2020/12/13

Go+serverlessで署名付きURLの取得

本記事はFusic Advent Calendar 2020の13日目の記事です。

昨日の記事は@fumikoba3さんのログファイルをCloudWatchで表示するでした。ログ出力の設定を丁寧に書かれているので是非ご一読ください。

さて、今回S3からファイルのアップロード・ダウンロードするための署名付きURLをAPI Gatewayにアクセスして取得するという構成をServerless FrameworkとGoで試してみたいと思います。

環境

Serverless Framework: 2.14.0

構成

以下のような構成にします。特定のパスへのアクセスがあったら署名付きURLをLambdaで取得し、それを用いて直接S3にアクセスするようにします。
今回は色々簡略化していますが、もし実運用を考えられる場合はcognitoでの認証等も検討する必要があると思います。

s3-pre-signed-diagram

実装

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のコンソールを確認してみると、アップロードされていることが確認できます。

s3-upload-check

また、しばらく時間が経過してから同様の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は最近勉強を進めているのですが、色々便利で楽しいですね。まだまだ精進したいと思います。

makihara

makihara

2020年からFusicでWebエンジニアをしています。 仕事ではPHPを主に扱っていて、最近はGoが楽しいです。