API Gatewayで受けたファイルをS3にユニークな名前で保存する。

# 既存Webサービスのファイルアップロード機能をサーバーレス化する

## 1. 背景

既存のWebサービスでは、ファイルアップロード系のAPIをサーバーレス化する必要が生じました。この記事では、その過程と実装方法について詳しく解説します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2193969/c936e2cc-82ad-77a8-9c08-4289ad9431e3.png)
[図1: 既存システムの構成図]

## 2. 構築

### 2.1 既存システムの構成

既存のシステムでは、ログイン、情報確認、ファイルアップロードの機能をApplication Load Balancer (ALB)、EC2インスタンス、RDSで実現していました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2193969/e6262fbb-1b0a-b14c-b831-f8f7d8c1d362.png)

[図2: 既存システムの詳細構成図]

### 2.2 CloudFrontによる振り分け

機能を切り出すにあたり、CloudFrontを使用してトラフィックの振り分けを行うことにしました。

#### 2.2.1 HTTPS to HTTPSの設定

CloudFrontで振り分ける際にHTTPS to HTTPSを行う場合、ALBに別途FQDNとACMの設定が必要になります。
既存のサービスへ切り戻しや切り替え時の通信断を減らすため既存のFQDN `service.sample.com` を残しつつ、FQDN( 'service-alb.sample.com' )を追加する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2193969/e1b51a3d-c33d-f96c-8806-05fcf01f7fee.png)


#### 2.2.2 API Gatewayのカスタムドメイン設定

今回切り分けるAPI Gatewayも、カスタムドメインで同様にFQDNとACMを設定しておきます。

custom domain: service-api.sample.com
API mapping: default(/) →　sample-api/prd/
### 2.3 API Gatewayの設定

API GatewayではREST APIのPOSTで通信を受け取り、AWSサービス統合を使用してS3にPUTリクエストを送信します。この際、パスオーバーライドを使用してS3のオブジェクトキーを動的に設定したい。

#### 2.3.1 初期の問題点

最初は以下のように設定しましたが、これでは常に同じファイル名で上書きされてしまう問題がありました。

```
my-bucket/upload/file
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2193969/b1235c43-f2e6-760b-0735-6ed28ca8d35d.png)

[スクリーンショット3: 初期のAPI Gateway設定画面]

#### 2.3.2 requestIDを使用した解決策

いろいろ試して、パスオーバーライドで変数を使う方法に行き着いた。`my-bucket/upload/{key}`のように設定し、requestIDでkeyを上書きすることにしました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2193969/4bf35e11-5b2f-40b0-a8a2-377935f9c8d5.png)
[スクリーンショット4: パスオーバーライドで変数定義]

マッピングテンプレートは作成後にメソッドを編集する画面でしか出てこないので、作成後に変更する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2193969/61db68c1-0421-a307-bff3-77f6b1478f29.png)

編集画面の一番下に、作成時にはなかったマッピングテンプレートが出てくる。
受信するデータのContent-Typeに合わせて設定を作成する。下記は：Content-Type: application/jsonの例
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2193969/d9fcc296-649d-1ba9-626f-dc60447ef4a5.png)

```velocity
#set($context.requestOverride.path.key=$context.requestId)
$input.body
```

#### 2.3.3 日付ベースのフォルダ構造の実装

さらに使いやすくするため、日付でフォルダを分ける構造を実装しました。
##### エポック日を使用した方法
ただ、サンプルを見てるとエポックミリ秒を使ったものが多く、日付ユーティリティが API Gatewayには実装されていない状態だったので、エポック日に方向転換（閏年とか月毎の月末日の違いとか考えるのが面倒）

```velocity
#set($EpochDay=$context.requestTimeEpoch/1000/3600/24)
#set($context.requestOverride.path.key="${EpochDay}/${context.requestId}")
$input.body
```

##### UTCの年月日を使用した方法
API Gateway開発者ガイドを参照し、`$context.requestTime`を使用してUTCベースの年月日フォルダ構造を実装しました。

[コードブロック3: UTCの年月日を使用したVTLマッピングテンプレート]
```velocity
#set($year=$context.requestTime.substring(7,11))
#set($monthstr=$context.requestTime.substring(3,6))
#if($monthstr=="Jan")
    #set(month=01)
#elseif($monthstr=="Feb")
    #set(month=02)
#elseif($monthstr=="Mar")
    #set(month=03)
#elseif($monthstr=="Apr")
    #set(month=04)
#elseif($monthstr=="May")
    #set(month=05)
#elseif($monthstr=="Jun")
    #set(month=06)
#elseif($monthstr=="Jul")
    #set(month=07)
#elseif($monthstr=="Aug")
    #set(month=08)
#elseif($monthstr=="Sep")
    #set(month=09)
#elseif($monthstr=="Oct")
    #set(month=10)
#elseif($monthstr=="Nov")
    #set(month=11)
#elseif($monthstr=="Dec")
    #set(month=12)
#end
#set($day=$context.requestTime.substring(0,2))
#set($context.requestOverride.path.key="${year}/${month}/${day}/${context.requestId}")
$input.body
```

## 3. 結論

この方法により、既存のWebサービスのファイルアップロード機能を効率的にサーバーレス化することができました。日付ベースのフォルダ構造により、アップロードされたファイルの管理も容易になりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2193969/af182109-e3b1-3252-f628-376f7bc7058c.png)

[図3: 最終的なシステム構成図]

## 4. 参考資料

1. [API Gateway開発者ガイド](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html)
2. [AWS再投稿: API Gateway call S3 put object using VTL](https://repost.aws/ja/questions/QUs_-XntwWT2yBJUsff4JwPQ/api-gateway-call-s3-put-object-using-vtl)