SAM/lambda/Pythonを使ってエクセル操作してみたいな〜というお話

# 最初に
pythonでエクセルやらスクレーピングやらを簡単にやってみたいなと思い、
まずはエクセルで簡単なファイル操作をしてみました。

今回、lambda以外は全て触るのが初めてなので微妙な記述等あるかと思いますが、
ご容赦いただければ幸いです。またSAMやAWS CLIなどのインストール等については割愛しておりますので、同じように試される方は適宜検索してみてください。

今回実現したいのは、
以下のように`curl`でエクセルファイルを添付して、APIGatewayを叩いて、lambdaで処理して、そのエクセルファイルから値を抽出して返却する、というシンプルなものです。

が、思った以上に大変でした。。。

![スクリーンショット 2025-01-20 11.34.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3960433/fdd1d334-55c8-ddcb-7653-1ebbece63db4.png)


# SAM
では早速SAMから定義していきます。

`sam init`を実行し、セットアップのために色々と聞かれるので、答えていく。
```.terminal
$ sam init

You can preselect a particular runtime or package type when using the `sam init` experience.
Call `sam init --help` to learn more.

Which template source would you like to use?
	1 - AWS Quick Start Templates
	2 - Custom Template Location
Choice: 1

Choose an AWS Quick Start application template
	1 - Hello World Example
	2 - Data processing
	3 - Hello World Example with Powertools for AWS Lambda
	4 - Multi-step workflow
	5 - Scheduled task
	6 - Standalone function
	7 - Serverless API
	8 - Infrastructure event management
	9 - Lambda Response Streaming
	10 - GraphQLApi Hello World Example
	11 - Full Stack
	12 - Lambda EFS example
	13 - Serverless Connector Hello World Example
	14 - Multi-step workflow with Connectors
	15 - DynamoDB Example
	16 - Machine Learning
Template: 1

Use the most popular runtime and package type? (python3.13 and zip) [y/N]: N

Which runtime would you like to use?
	1 - dotnet8
	2 - dotnet6
	3 - go (provided.al2)
	4 - go (provided.al2023)
	5 - graalvm.java11 (provided.al2)
	6 - graalvm.java17 (provided.al2)
	7 - java21
	8 - java17
	9 - java11
	10 - java8.al2
	11 - nodejs22.x
	12 - nodejs20.x
	13 - nodejs18.x
	14 - python3.9
	15 - python3.8
	16 - python3.13
	17 - python3.12
	18 - python3.11
	19 - python3.10
	20 - ruby3.3
	21 - ruby3.2
	22 - rust (provided.al2)
	23 - rust (provided.al2023)
Runtime: 15

# ここで今回はイメージを選択しました。
What package type would you like to use?
	1 - Zip
	2 - Image
Package type: 2

Based on your selections, the only dependency manager available is pip.
We will proceed copying the template using pip.

Would you like to enable X-Ray tracing on the function(s) in your application?  [y/N]: n

Would you like to enable monitoring using CloudWatch Application Insights?
For more info, please view https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch-application-insights.html [y/N]: n

Would you like to set Structured Logging in JSON format on your Lambda functions?  [y/N]: y
Structured Logging in JSON format might incur an additional cost. View https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs.html#monitoring-cloudwatchlogs-pricing for more details

Project name [sam-app]: excel-app

    -----------------------
    Generating application:
    -----------------------
    Name: excel-app
    Base Image: amazon/python3.8-base
    Architectures: x86_64
    Dependency Manager: pip
    Output Directory: .
    Configuration file: excel-app/samconfig.toml

    Next steps can be found in the README file at excel-app/README.md
    

Commands you can use next
=========================
[*] Create pipeline: cd excel-app && sam pipeline init --bootstrap
[*] Validate SAM template: cd excel-app && sam validate
[*] Test Function in the Cloud: cd excel-app && sam sync --stack-name {stack-name} --watch
```

すると以下のようなディレクトリとファイルが生成されます。
ここで2点追加操作します。
- 送信用のエクセルファイルを追加（何でもOK）
- hello_wordからexcel_manipulatorにディレクトリ名を変更


ここのポイントとしては`What package type would you like to use?`の質問で`Image`を使用することです。

```
cd excel-app
tree

.
├── Book1.xlsx # 検証用に手動で後から追加しました。
├── README.md
├── __init__.py
├── events
│   └── event.json
├── excel_manipulator # hello_worldから変更
│   ├── Dockerfile
│   ├── __init__.py
│   ├── app.py
│   └── requirements.txt
├── samconfig.toml
├── template.yaml
└── tests
    ├── __init__.py
    └── unit
        ├── __init__.py
        └── test_handler.py
```

# template.yaml
次に`template.yaml`を変更します。
```.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  python3.8

  Sample SAM Template for excel-app

# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
  Function:
    # 時間がかかる処理をしたい場合は、ここを伸ばす必要があります。
    Timeout: 3
    MemorySize: 128

    # You can add LoggingConfig parameters such as the Logformat, Log Group, and SystemLogLevel or ApplicationLogLevel. Learn more here https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-function.html#sam-function-loggingconfig.
    LoggingConfig:
      LogFormat: JSON
Resources:
  # RestAPIの定義
  ExcelManipulatorApi:
    Type: AWS::Serverless::Api
    Properties:
      StageName: default
      BinaryMediaTypes:
        - '*~1*'

  ExcelManipulatorFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: 
    Properties:
      PackageType: Image
      Architectures:
      - x86_64
      Events:
        ExcelManipulatorApi:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            Path: /excel_manipulator
            Method: post
            RestApiId: !Ref ExcelManipulatorApi

    Metadata:
      Dockerfile: Dockerfile
      DockerContext: ./excel_manipulator # ここをhello_worldから変更する
      DockerTag: python3.8-v1

Outputs:
  # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
  # Find out more about other implicit resources you can reference within SAM
  # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api
  ExcelManipulatorApi:
    Description: API Gateway endpoint URL for Prod stage for ExcelManipulator Function
    Value: !Sub "https://${ExcelManipulatorApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/excel_manipulator/"
  ExcelManipulatorFunction:
    Description: ExcelManipulator Lambda Function ARN
    Value: !GetAtt ExcelManipulatorFunction.Arn
  ExcelManipulatorFunctionIamRole:
    Description: Implicit IAM Role created for ExcelManipulator Function
    Value: !GetAtt ExcelManipulatorFunctionRole.Arn
```

細かくは解説しませんが、ポイントが数点あります（めちゃくちゃ沼った。。。）
## BinaryMediaTypesを指定する
これをAPIに指定しないといけません。
なぜなら、RestAPIでは、デフォルトではUTF-8のテキストのペイロードしかサポートしておらず、これを設定しないとめちゃくちゃ文字化けして500エラーが返却されます。

こんな感じで文字化けします。
![スクリーンショット 2025-01-20 18.21.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3960433/cbab3b85-dddc-b303-5646-8bc1e091b44f.png)

HTTPAPIにすれば問題ないのですが、今回はRESTAPIで進めるためにこの設定を付与しました。

また以下のドキュメントに書かれている通り`/`は`~1`と書く必要があるので注意が必要です。
https://docs.aws.amazon.com/apigateway/latest/api/API_RestApi.html#apigw-Type-RestApi-binaryMediaTypes:~:text=Required%3A%20No-,binaryMediaTypes,-The%20list%20of

# app.py
次にコードを変更します。特に注意点はありません。
Base64でエンコードされているので、それをデコードする、くらいでしょうか。
```.py
import json
import pandas as pd
import base64
from io import BytesIO

def lambda_handler(event, context):
    print("event", event)
    # Base64エンコードされたエクセルデータを取得
    body = event['body']
    print("body_data", body)
    
    # Base64デコードしてバイナリデータに変換
    excel_data = base64.b64decode(body)

    # BytesIOを使ってメモリ上でエクセルファイルを開く
    excel_file = BytesIO(excel_data)

    df = pd.read_excel(excel_file, engine='openpyxl')
    output = df.loc[:,:]
    print("output", output)
    
    # DataFrame を JSON に変換
    output_json = output.to_json(orient="records")  # レコード形式で変換
    print("output_json", output_json)

    # Lambda のレスポンスとして返す
    return {
        "statusCode": 200,
        "body": json.dumps({"output": json.loads(output_json)})  # JSON を辞書形式にして返す
    }

```

# requirements.txtの変更
railsでいうgemfile的な位置付けのファイルなんですかね。
ここに必要なパッケージを記述します。
```requirements.txt
requests
pandas
```

# ビルドしてデプロイする
ここまでで準備は整ったので、ビルドとデプロイをしていきます。

ビルド
```
$ sam build
```
---
デプロイ
```
$ sam deploy -g --profile ~~~
```
また、色々と確認されるので答えていく。基本、全て`Y`で良いと思います。
`ExcelManipulatorFunction has no authentication. Is this okay? [y/N]: y`については、本来は設定するべきですが、今回は割愛しています。
```
Configuring SAM deploy
======================

	Looking for config file [samconfig.toml] :  Found
	Reading default arguments  :  Success

	Setting default arguments for 'sam deploy'
	=========================================
	Stack Name [image-sam-app]: 
	AWS Region [us-east-1]: ap-northeast1
	#Shows you resources changes to be deployed and require a 'Y' to initiate deploy
	Confirm changes before deploy [Y/n]: Y
	#SAM needs permission to be able to create roles to connect to the resources in your template
	Allow SAM CLI IAM role creation [Y/n]: Y
	#Preserves the state of previously provisioned resources when an operation fails
	Disable rollback [y/N]: y
	ExcelManipulatorFunction has no authentication. Is this okay? [y/N]: y
	Save arguments to configuration file [Y/n]: y
	SAM configuration file [samconfig.toml]: 
	SAM configuration environment [default]: 
```

しばらく待って以下のメッセージが表示されたらデプロイ成功です！
```
Successfully created/updated stack - excel-app in ap-northeast-1
```
---

AWSのコンソール画面に確認しにいくと
- lambda関数
- API Gateway
が存在することを確認できました。

試しにcurlで最初に用意したエクセルファイルを送ると、
```
$ curl -X POST \
     -H "Content-Type: multipart/form-data" \
     -F "file=@Book1.xlsx" \
     https://~~~~~.execute-api.ap-northeast-1.amazonaws.com/Stage/excel_manipulator
```

こんな感じでエクセルから文字列を読み取って返却されました🎉
```
{"output": [{"A1": "A2", "B1": "B2", "C1": "C2"}, 
            {"A1": "A3", "B1": "B3", "C1": "C3"}]}
```

# まとめ
振り返ってみるとめちゃくちゃ簡単そう（という簡単）なのですが、
- Layerを作成してLambdaに設定したらいい
- dockerを起動して、そこでpip installしたらいい
などさまざまな情報にあたり、色々試した結果今回の方法に辿り着きました。

同じように詰まっている方の助けになれば幸いです。

# 参考
https://docs.aws.amazon.com/apigateway/latest/api/API_RestApi.html#apigw-Type-RestApi-binaryMediaTypes

https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/sam-resource-api.html#sam-api-binarymediatypes

https://qiita.com/hayao_k/items/f8c7ad5e35e29d590957