API Gateway + Lambda + S3で音楽ファイルのタグ情報を取得するアプリを作成する

ローカルに持っている音楽ファイルのタグ情報（曲名・アーティスト名・アートワーク）を取得する簡単なアプリケーションを作成したので、実装の流れを記事にまとめます。

今回はAWSの学習も兼ねて、API Gateway、Lambda、S3（Presigned URL）を組み合わせて実装してみました。

タグ情報の抽出には、mutagenというPythonライブラリをLambda内で用いています。

* この記事では主に実装の流れを説明していますが、Presigned URL(以下署名付きURL)、mutagenについても軽く説明しています。
* 機能実装の流れをわかりやすくするため、セキュリティ面の処理の実装を一部省いています（実際は許可されたユーザーだけがAPIエンドポイントにアクセスできるようにする必要があるなど）。


# 目次

- 実装機能の構成
- 音楽ファイルをS3にアップロードする
  - 署名付きURLとは
  - S3バケットの作成
  - ロールの作成
  - Lambda関数の作成
  - API Gatewayの作成
  - クライアント側の処理
- 音楽ファイルのタグ情報を取得する
  - mutagenとは
  - Lambda関数の作成
  - Lambda Layerの作成
  - API Gatewayの作成
  - クライアント側の処理
- まとめ


# 実装機能の構成

<img width="600px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2050533/9ec86a86-8b15-06e3-90b1-0567d87bc0a6.png">

1. 署名付きURLを取得する
2. 署名付きURLを使ってS3に音声ファイルをアップロードする
3. S3の音声ファイルからタグ情報を取得する

といった流れになります。

ここで、S3を介さず、Lambdaに直接音楽ファイルを渡せば良いと思われる方がいるかもしれません。
しかし、この手法は取ることができませんでした。

理由は、API Gatewayに10MB、Lambdaに6MBのペイロード制限があるためです。ほとんどの音楽ファイルは6MBを超えるので、そのままPOSTすることができません。


# 音楽ファイルをS3にアップロードする

次の2つの処理を実装します。

1. 署名付きURLを取得
2. 取得した署名付きURLを使ってS3に音声ファイルをアップロード

こちらを参考にしています。
* [S3 Presigned URLの仕組みを調べてみた](https://qiita.com/tmiki/items/87697d3d3d5330c6fc08)
* [Lambda + S3 Presigned URLを用いてS3バケットにファイルをアップロード](https://qiita.com/kei_japan/items/00266727b64e1167c4f3)


## 1.署名付きURLとは

署名付きURLとは、S3上の特定のオブジェクトに期限付きのURLを発行する機能です。
このURLを利用することで、制限時間内であれば誰でも、アクセス制限されているオブジェクトの取得や更新を行うことができます。

そのため普段はS3を非公開にしておき、アップロードが必要な時だけ、特定のユーザーに制限時間付きで権限を与えるといった使い方ができます。

また、クライアント側から直接S3にアップロードするため、API GatewayやLambdaなどの制約を受けず、大容量のファイルをアップロードすることができます。


## 2.S3バケットの作成

音声ファイルをアップロードするS３バケットを作成していきます。

S3を開き、「バケット」から「バケットの作成」をクリックします。
![スクリーンショット 2022-01-24 19.16.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2050533/48171c8d-d437-7b13-8362-e81281d23fb8.png)

バケットの設定画面が開くので、バケット名を適宜入力した後、「バケットの作成」をクリックします。

バケット一覧から作成したバケットを開き、「アクセス許可」から「Cross-Origin Resource Sharing (CORS)」を以下のように編集します。

```
[
    {
        "AllowedHeaders": [
            "*"
        ],
        "AllowedMethods": [
            "GET",
            "PUT"
        ],
        "AllowedOrigins": [
            "*"
        ],
        "ExposeHeaders": []
    }
]
```
これでバケットの準備ができました。


## 3.ロールの作成

LambdaにS3へのアクセス権を与えるIAMロールを作成していきます。

IAMを開き、「ロール」から「ロールを作成」をクリックします。
![スクリーンショット 2022-01-17 21.04.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2050533/17d8b7af-3677-cf6c-5949-cd77c5913906.png)

「AWSサービス」から「lambda」を選択します。
![スクリーンショット 2022-01-17 21.06.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2050533/7c21e476-8719-c02b-926d-d6074854b703.png)

アクセス権限ポリシーとして「AmazonS3FullAccess」を選択します。
その後タグの設定に移りますが、タグはいらないので何も入力せず次に進んでください。
![スクリーンショット 2022-01-17 21.11.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2050533/2725def1-1e9c-7b1e-4c98-8c6e0e51e537.png)

ロール名は何でもいいですが、今回はS3FullAccessとしておきます。
ロールの作成を押して完了です。
![スクリーンショット 2022-01-17 21.13.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2050533/15ab42ce-d86b-501b-88b0-961bff2da456.png)


## 4.Lambda関数の作成

S3から署名付きURLを取得するLambda関数を作成していきます。

Lambdaを開き、「関数」から「関数の作成」をクリックします。
![スクリーンショット 2022-01-17 21.16.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2050533/5d2d990e-ebb8-46f9-fdd5-3417d3fe6236.png)

「1から作成」を選択し、以下のように入力した後、「関数の作成」をクリックします。
・名前:PresignedUrlGetter
・ランタイム:Python3.9
・アーキテクチャ：x84_64
・既存のロール:S3FullAccessを選択
![スクリーンショット 2022-01-17 21.31.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2050533/3db4f472-a556-61b0-d275-39bb490dc451.png)

Lambda関数の作成が完了した後、コードを以下のように編集すれば完了です。

```python:lambda_function.py
import boto3
from botocore.client import Config
import json
import uuid

def lambda_handler(event, context):

    PUT_BUCKET = 'your-s3-bucket'   #S3バケット名
    UUID = str(uuid.uuid4())   #一意なランダム値を生成
    PUT_KEY = 'audio/' + UUID   #S3バケット上のファイルの保存先
    BORROW_TIME = 100   #署名付き　URLの制限時間
    
    s3 = boto3.client('s3', region_name='ap-northeast-1', config=Config(signature_version='s3v4'))

    #バケット上に空ファイルを作成
    s3.put_object(
        Bucket=PUT_BUCKET,
        Key=PUT_KEY
    )

    #空ファイルへのアップロードを可能にする署名付きURLを生成
    put_url = s3.generate_presigned_url(
        ClientMethod = 'put_object', 
        Params = {'Bucket' : PUT_BUCKET, 'Key' : PUT_KEY}, 
        ExpiresIn = BORROW_TIME, 
        HttpMethod = 'PUT')

    #署名付きURLとuuidを返す
    return {
        'statusCode': 200,
        'isBase64Encoded': False,
        'headers': {
            'Access-Control-Allow-Origin': '*',
            'Access-Control-Allow-Methods': 'OPTIONS,GET'
        },
        'body': json.dumps(
            {
                'put_url': put_url,
                'uuid': UUID
            }
        )
    }
```


## 5.API Gatewayの作成

先ほど作成したLambda関数と紐付けるAPI Gatewayを作成していきます。

「API Gateway」から「APIを作成」をクリックします。
<img width="500px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2050533/5943eaf6-17f5-16a0-2348-70a84732f302.png">

「REST API」の「構築」をクリックします。
<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2050533/d9b54d18-61b2-abcd-b5d5-d511a41c5439.png">

以下のように入力した後、「APIの作成」をクリックします。API名と説明は何でも大丈夫です。
![スクリーンショット 2022-01-17 21.38.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2050533/4907c961-4426-7ce5-8270-eb4fe3569240.png)

「アクション」から「メソッドの作成」を選択すると、メソッドを選択するタブが出てくるため、「GET」を選択し、チェックボタンを押します。
<img width="500px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2050533/e0ba7759-d774-e3fd-603b-c48d88e19395.png">

以下のように選択し、保存します。
![スクリーンショット 2022-01-17 21.42.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2050533/73a1e47a-021a-5cb3-439c-5765a5a74167.png)

「アクション」から「CORSの有効化」を選択すると、設定画面が表示されるので、そのまま「CORSを有効にして既存のCORSを置換」をクリックします
<img width="500px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2050533/fcbce9ca-1897-d5b1-c421-fdbbe74e62c3.png">

「アクション」から「APIのデプロイ」を選択し、ステージ名を適当に入力した後「デプロイ」をクリックします。
<img width="350px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2050533/73218fb1-89df-7b99-59bc-43626007b650.png">

初回デプロイ時にAPIのエンドポイント(URL)が生成され、APIの「ステージ」から確認することができます。
以後このURLにGETリクエストを送り、署名付きURLを取得します。


## 6.クライアント側の処理

クライアント側から署名付きURLを取得し、音声ファイルをアップロードします。
今回は`input`タグで選択されたファイルをアップロードしてみます。

```html:index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
</head>
<body>
  <input type='file' id='file-input'>

  <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
  <script>
    async function uploadFile(event) {

    //ファイルを取得する
    const audio_file = event.target.files[0];

    //APIエンドポイントにGETリクエストを送る
    const res_signed_url = await axios.get('https://~~~~~~~.execute-api.ap-northeast-1.amazonaws.com/~~~~~~~~');

    //レスポンスから署名付きURLを取り出す
    const signed_url = JSON.parse(res_signed_url.data.body).put_url;

    //署名付きURLにファイルをPUTする
    await axios.put(
            signed_url,
            audio_file,
            {
              headers: {
                'Content-Type': audio_file.type
              }
            }
          );
    };

    const input = document.getElementById('file-input');

    input.addEventListener('change', (event) => uploadFile(event));
  </script>
</body>
</html>
```
S3バケットにファイルがアップロードされていれば成功です。


# 音楽ファイルのタグ情報を取得する

S3にアップロードした音楽ファイルからタグ情報を取得する処理を実装します。

タグ情報とは、音楽ファイルに埋め込まれているアーティスト名やアートワークなどのメタデータです。
今回はそのタグ情報の取得に便利なmutagenというライブラリをLambda内で使います。


## 7.mutagenとは

mutagenは、音声ファイルのメタデータを取得・編集するPythonライブラリです。
MP3、MP4、FLAC、AIFFなど主要な音声ファイル形式はほぼ全てサポートしています。

例えば、`.m4a`拡張子の音楽ファイルのタグ情報を取得する場合は以下のようになります。

```python:example.py
import mutagen.mp4

tags = mutagen.mp4.MP4('file_path') #file_pathに音声ファイルのファイルパスを指定

title          = tags.get('\xa9nam')[0]
album          = tags.get['\xa9alb'][0]
album_artist   = tags.get["aART"][0]
artist         = tags.get('\xa9ART')[0]
art_work       = tags.get('covr')[0]
genre          = tags.get['\xa9gen'][0]
year           = tags.get['\xa9day'][0]
```
このように様々なタグ情報を簡単に取得することができます。

取得については、
[Mutagenチートシート: MP3, AAC (.m4a), FLACのタグ・メタデータ抽出](https://www.wizard-notes.com/entry/python/mutagen-cheatsheet)
にわかりやすくまとめられていました。

より詳しく知りたい場合は[公式ドキュメント](https://mutagen.readthedocs.io/en/latest/)を参考にしてください。


## 8.Lambda関数の作成

音楽ファイルのタグ情報を取得しクライアントに返す処理を行うLambdaを作成していきます。

Lambdaを開き、先ほどと同様に新しく関数を作成します。
今回は以下のように入力し、「関数の作成」をクリックします。

・名前:AudioTagsGetter
・ランタイム:Python3.7 (使用するライブラリの都合上3.7にしています)
・アーキテクチャ：x84_64
・既存のロール:S3FullAccessを選択
![スクリーンショット 2022-01-24 19.54.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2050533/2b36a231-0aac-7fc7-495f-6f0a2f1cbc66.png)

作成できたら「設定」の「一般設定」から「編集」をクリックします。
![スクリーンショット 2022-01-25 13.42.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2050533/50b84181-c6b5-a035-a87b-5ba6d5551440.png)

念の為「タイムアウト」を0分30秒にして保存します。(サイズの大きい音楽ファイルに対応可能にするため)
![スクリーンショット 2022-01-25 13.44.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2050533/b4958a1f-4040-a902-656b-99d256bf48a5.png)

コードを以下のように編集すれば完了です。
※今回は音楽ファイルが`.m4a`拡張子の場合の処理を実装しています。

```python:lambda_function.py
import json
import boto3
import uuid
import base64
import os

import mutagen.mp4
from io import BytesIO
from PIL import Image


s3 = boto3.resource('s3')
s3_client = boto3.client('s3')

def lambda_handler(event, context):

  bucket = s3.Bucket('bucket_name')
  
  UUID = event['uuid']
  key_audio = 'audio/' + UUID
  audio_file_path = f'/tmp/{UUID}.m4a'
  
  #tmpディレクトリに音声ファイルをダウンロード
  bucket.download_file(key_audio, audio_file_path)
  
  #タグ情報を取得
  tags = mutagen.mp4.MP4(audio_file_path)
  apic = tags.get('covr')
  
  #音声ファイルは不要になったため削除
  os.remove(audio_file_path)
  s3_client.delete_object(Bucket='audio-tmp-bucket', Key=key_audio)
  
  #アートワークがあるときはその他の情報を取得
  if apic:
    title = tags.get('\xa9nam')
    artist = tags.get('\xa9ART')
    apic = apic[0]
      
    if title: title = title[0]
    else: title  = ''
    if artist: artist = artist[0]
    else: artist = ''

  else:
    return {
      'statusCode': 200,
      'headers': {
          'Access-Control-Allow-Headers': 'Content-Type',
          'Access-Control-Allow-Origin': '*',
          'Access-Control-Allow-Methods': 'OPTIONS,POST'
      },
      'body': None
    }
  
  #取得したアートワークをbase64エンコード
  cover_img = BytesIO(apic)
  b64_img = str(base64.b64encode(cover_img.read()))[2:-1]
    

  return {
    'statusCode': 200,
    'headers': {
        'Access-Control-Allow-Headers': 'Content-Type',
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Methods': 'OPTIONS,POST'
    },
    'body': json.dumps({
      "artist": artist,
      "title": title,
      "uuid": UUID,
      "image": b64_img
    })
  }
```


## 9.Lambda Layerの作成

Lambdaの環境には`mutagen`と`Pillow(PIL)`は標準インストールされていないので、そのままではこれらのライブラリを使用できません。
そこで、`Lambda Layer`にこれらのライブラリをアップロードし使用可能にします。

ローカルにインストールしたライブラリをアップロードするのが一般的な方法ですが、残念ながらこの方法だとPillowは動きません。

Pillowにはインストールする OS に依存する部分があるため、Lambdaと同じ環境の、**Amazon** **Linux上でインストールしたPillowが必要**だからです。

こちらを参考にしています。
[【AWS】LambdaでPillowを使う方法(Lambda Layer)](https://predora005.hatenablog.com/entry/2021/05/26/190000)

---
Amazon Linux2をOSとするEC2サーバーを用意してください。(構築については省略します)

EC２サーバーにssh接続した後、python3.7をインストールします。(python3.8だとPillowのインストールに手間がかかるため)

```
$ sudo amazon-linux-extras install python3.7
```
次にpythonディレクトリを作成し、Pillowとmutagenをインストールします。

```
$ mkdir python
$ pip3.7 install Pillow mutagen -t python
```

Pillowとmutagenがインストールされたpythonディレクトリをzip化します。

```
$ zip -r PIL_and_mutagen.zip python
```

EC2からexitし、EC2上のPIL_and_mutagen.zipをローカル(今回はデスクトップ)にダウンロードします。
* [SCPコマンドで、EC2インスタンスにあるファイルのダウンロード/アップロードを秒で対応するために](https://qiita.com/Takao_/items/902ce0b12bb8fe74069e)

```
$ scp -i ~/.ssh/~~~~~~~~~.pem ec2-user@1.234.567.89:/home/ec2-user/PIL_and_mutagen.zip ~/Desktop
```

これで、Lambda layerにアップロードするzipファイルが作成できました。
次にレイヤーを作成します。

Lambdaを開き「レイヤー」から「レイヤーを作成」をクリックします。
![スクリーンショット 2022-01-24 21.54.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2050533/dacb9e2c-5940-b1e1-cfb1-53a7c3705b56.png)

以下のように入力し、作成をクリックすれば、レイヤーが作成されます。
![スクリーンショット 2022-01-24 21.56.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2050533/4e53f7e7-6d1e-c01c-8468-0fdc41e0ac8b.png)

関数(AudioTagsGetter)から「コード」を開き、下の方にある「レイヤー」から「レイヤーの追加」をクリックします。
![スクリーンショット 2022-01-24 22.04.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2050533/f7b77b9a-054a-4264-a524-ca1f9dd8e6b8.png)

以下のように選択し、「追加」をクリックします。
![スクリーンショット 2022-01-24 22.05.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2050533/cf17acdd-9e3e-e786-893c-372d39c7a0ca.png)

追加されました。これでLambdaでmutagenとPILを使用できるようになりました。
![スクリーンショット 2022-01-24 22.05.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2050533/f54096f8-d799-f122-b0af-a6f49ae9bb19.png)


## 10.API Gatewayの作成

Lambda関数(AudioTagsGetter)の窓口となるAPI Gatewayを作成していきます。

先ほどと同様に、「API Gateway」を開き、「APIを作成」から「REST API」の「構築」をクリックします。
<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2050533/d9b54d18-61b2-abcd-b5d5-d511a41c5439.png">

「アクション」から「メソッドの作成」を選択するとメソッドを選択するタブが出てくるので、今回は「POST」を選択し、チェックボタンを押します。
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2050533/277f8450-6f83-0835-8d1b-7ace50a2c887.png">

以下のように入力し保存します。
![スクリーンショット 2022-01-25 11.53.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2050533/86719b08-83fe-249a-35be-80478053f59e.png)

「メソッドの実行」の「統合リクエスト」をクリックします。
![スクリーンショット 2022-01-25 15.17.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2050533/0e0e9d2b-0864-a2bf-f8d1-dbe6e9b3e374.png)

統合リクエストの設定画面が開くので、下の方にある「マッピングテンプレート」から以下のようにマッピングテンプレートを追加します。
![スクリーンショット 2022-01-25 12.03.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2050533/b7577606-2821-f964-d2f5-ae177158556f.png)

テンプレートを以下のように編集して保存を押します。
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2050533/6dc06e03-5c3c-99f3-dda3-467cf52c684a.png">

その後、先ほどと同様に「アクション」からCORSを有効化します。

「アクション」からAPIのデプロイを行うとAPIエンドポイントが作成されます。
これで、このURLにS3上の音楽ファイル名(uuid)を`POST`することでタグ情報を取得できるようになりました。

## 11.クライアント側の処理

クライアント側から、S3にアップロードした音楽ファイルのタグ情報を取得します。
`script`タグの中に追記していきます。

```html:index.html
  <script>
    async function uploadFile(event) {

      //署名付きURLにファイルをPUTするまでを省略

      //APIエンドポイントにPOSTリクエストを送る
      const uuid = (JSON.parse(res_signed_url.data.body)).uuid;
      const res_audio_info = await axios.post('https://~~~~~~~.ap-northeast-1.amazonaws.com/~~~~~~~', { uuid: uuid });
      
      //レスポンスからタグ情報を取り出す
      const audio_info = JSON.parse(res_audio_info.data.body);

      console.log(audio_info.title);
      console.log(audio_info.artist);

      //アートワークの画像ファイルを取得
      fetch('data:image/jpeg;base64,' + audio_info.image)
        .then(response => response.blob())
        .then(blob => new File([blob], audio_info.uuid + '.jpeg'))
        .then(file => {
            //fileはFileオブジェクト
            console.log(file)
          }
        )
    };

    const input = document.getElementById('file-input');

    input.addEventListener('change', (event) => uploadFile(event));
  </script>
```
ブラウザでファイルをインプットした数秒後、タグ情報がコンソールに表示されれば成功です。


# まとめ

AWSを使って、クライアント側から音楽ファイルをアップロードしタグ情報を取得する簡単なアプリを作ることができました。

今回はこのように実装しましたが、今度学習を兼ねてサーバーを立てて同じ機能を実装してみようと思います。

参考記事
・[【AWS】 S3 Presigned URLの仕組みを調べてみた](https://qiita.com/tmiki/items/87697d3d3d5330c6fc08)
・[Lambda + S3 Presigned URLを用いてS3バケットにファイルをアップロード](https://qiita.com/kei_japan/items/00266727b64e1167c4f3)
・[Mutagenチートシート: MP3, AAC (.m4a), FLACのタグ・メタデータ抽出](https://www.wizard-notes.com/entry/python/mutagen-cheatsheet)
・[mutagen公式ドキュメント](https://mutagen.readthedocs.io/en/latest/)
・[【AWS】LambdaでPillowを使う方法(Lambda Layer)](https://predora005.hatenablog.com/entry/2021/05/26/190000)