S3操作をするLambda関数(Python-image)をSAM CLIを使ってデプロイする手順をまとめてみる

##はじめに
Lambda関数を作成するにあたり、必要な手順をざっくりまとめました。
以下も試しました。

- 1つのアプリケーションで複数の関数の定義
- S3の操作周りをLambdaで行う
- SAM CLIのローカルテストを試す

##開発者のIAMユーザ作成
開発とデプロイで使用します。
![スクリーンショット 2021-09-04 3.33.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/35a521dc-8ae5-66ad-5764-947364e38860.png)
![スクリーンショット 2021-09-04 3.38.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/399a653b-7a69-e8ea-447f-0ce564bf8239.png)
あとは、デフォルトのまま作成し、アクセスキー、シークレットアクセスキーを保存

#####必要な場合アクセス権を追加
ユーザ作成時に追加した「AWSLambda_FullAccess」のみなので必要なものがあれば追加します。

|  権限  |  概要  |
| ---- | ---- |
|  AWSLambda_FullAccess  |  Lambda、CloudWatchなど  |
|  AmazonS3FullAccess  |  S3  |
|  AmazonAPIGatewayAdministrator  |  API Gatewayの作成やデプロイに必要なポリシー  |
|  AmazonEC2ContainerRegistryFullAccess | ECRの作成やデプロイに必要なポリシー　|
|  IAMFullAccess | AWSCloudFormationの作成やデプロイに必要なポリシー |
|  AWSCloudFormationFullAccess | AWSCloudFormationの作成やデプロイに必要なポリシー |

##実行ロールの作成
Lambda関数を実行するロールです。デプロイ時に指定しますのでしばらく使いません。
![スクリーンショット 2021-09-04 4.21.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/5e0c40a1-44c7-f33d-cdce-3922b10b12f5.png)
![スクリーンショット 2021-09-04 4.22.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/bf9ce655-5100-8576-197e-e9f50ef2cbe3.png)
![スクリーンショット 2021-09-04 4.25.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/4f50bea5-261b-2ebf-db40-aaf7a81a0353.png)
![スクリーンショット 2021-09-04 4.28.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/9a1ae6df-e9d4-3612-5781-3037daf6c304.png)

#####実行ロールにも必要な場合アクセス権を追加
Lambda実行時の権限で必要なものがあれば設定します。インラインポリシーを使うと最低限のポリシーを設定できます。
例）Lambdaから特定のS3のファイル取得を行う場合
![スクリーンショット 2021-09-06 16.17.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/2e03c2f9-36b7-61bf-d0d1-f220da2b3c0e.png)

※実際には、S3アップロードを行う必要があるので`AmazonS3FullAccess`をつけています。

##まずはLambdaとは関係ないプログラムを書いてみる
S3を操作するPythonプログラムのサンプルです。

#####テスト読み取り用バケットにテスト用画像をアップしておく
ローカル実行用にテスト読み取り用バケットとテスト用画像をアップしておきます。
![スクリーンショット 2021-09-06 19.36.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/cb132313-84f1-590d-7d07-06c06f7a16e6.png)
Lambda関数とトリガーで紐づけられるS3は新たに作成する必要があるため、実際にはこのバケットは使いません。

環境はこちらの記事のosイメージをpythonに変えたものですが、なんでも構いません。
aws認証情報には、先ほど作成した開発者のIAMユーザを使います。

https://qiita.com/c3drive/items/348a99c1ea92d4ce246f

```bash:必要なライブラリをインストールする
$ pip install boto3
```
単に指定したS3バケットの画像「port.png」を取得してリネームし、書き込み用バケットにアップするプログラムです。

```python:s3example.py
import boto3
import tempfile

s3 = boto3.resource('s3')
s3_write_bucket = '{書き込み用バケット}'


def hello(s3_read_bucket, filename):
    # ファイルの読み込み
    obj = s3.Object(s3_read_bucket, filename)
    response = obj.get()
    tmpdir = tempfile.TemporaryDirectory()
    fp = open(tmpdir.name + '/' + filename, 'wb')
    fp.write(response['Body'].read())
    fp.close;

    # ファイル名に.zipをつけてS3にアップロード
    zipname = tempfile.mkstemp()[1]
    obj = s3.Object(s3_write_bucket, filename + '.zip')
    response = obj.put(
        Body=open(tmpdir.name + '/' + filename, 'rb')
    )

    tmpdir.cleanup()
    return response


if __name__ == '__main__':
    print(hello("{テスト読み取り用バケット}", "port.png"))
```
動作確認します。

```bash
$ python s3example.py
```
アップされました。
![スクリーンショット 2021-09-07 0.23.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/152977bd-b20d-e3ea-545b-0b576fd3ac5f.png)


##Lambda関数にする
`lambda_handler`関数を追加します。
トリガーから渡される`event`はS3オブジェクトの更新です。
中身については、「ローカルテスト」にて後述します。

```python:s3example.py
import boto3
import tempfile

s3 = boto3.resource('s3')
s3_write_bucket = '{書き込み用バケット}' #循環参照防止のためreadとは別のバケット

############# 追加部分 #############
def lambda_handler(event, context):
    for rec in event['Records']:
        filename = rec['s3']['object']['key']
        s3_read_bucket = rec['s3']['bucket']['name']
        hello(s3_read_bucket, filename)
    return {
        "statusCode": 200,
    }
############# 追加部分 #############


def hello(s3_read_bucket, filename):
    # ファイルの読み込み
    obj = s3.Object(s3_read_bucket, filename)
    response = obj.get()
    tmpdir = tempfile.TemporaryDirectory()
    fp = open(tmpdir.name + '/' + filename, 'wb')
    fp.write(response['Body'].read())
    fp.close;

    # ファイル名に.zipをつけてS3にアップロード
    zipname = tempfile.mkstemp()[1]
    obj = s3.Object(s3_write_bucket, filename + '.zip')
    response = obj.put(
        Body=open(tmpdir.name + '/' + filename, 'rb')
    )

    tmpdir.cleanup()
    return response


if __name__ == '__main__':
    print(hello("{テスト読み取り用バケット}", "port.png"))
```
#####ローカルテスト
まず、コンソールのLambdaの「テスト」から「s3-put」のものを取得します。
![スクリーンショット 2021-09-07 3.24.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/896fe81f-4b93-3e5c-01d4-6feceb2f908e.png)

取得した情報を自身の環境に変更したものをfixtureにし、テストプログラムを作成しました。

```python:tests/unit/test_s3example.py
import pytest

from hello_world import s3example


@pytest.fixture()
def apigw_event():
    return {
        "Records": [
            {
            "eventVersion": "2.0",
            "eventSource": "aws:s3",
            "awsRegion": "ap-northeast-1", # 変更部分
            "eventTime": "1970-01-01T00:00:00.000Z",
            "eventName": "ObjectCreated:Put",
            "userIdentity": {
                "principalId": "EXAMPLE"
            },
            "requestParameters": {
                "sourceIPAddress": "127.0.0.1"
            },
            "responseElements": {
                "x-amz-request-id": "EXAMPLE123456789",
                "x-amz-id-2": "EXAMPLE123/5678abcdefghijklambdaisawesome/mnopqrstuvwxyzABCDEFGH"
            },
            "s3": {
                "s3SchemaVersion": "1.0",
                "configurationId": "testConfigRule",
                "bucket": {
                "name": "{テスト読み取り用バケット}", # 変更部分
                "ownerIdentity": {
                    "principalId": "EXAMPLE"
                },
                "arn": "arn:aws:s3:::{テスト読み取り用バケット}" # 変更部分
                },
                "object": {
                "key": "port.png", # 変更部分
                "size": 1024,
                "eTag": "0123456789abcdef0123456789abcdef",
                "sequencer": "0A1B2C3D4E5F678901"
                }
            }
            }
        ]
    }


def test_lambda_handler(apigw_event, mocker):
    ret = s3example.lambda_handler(apigw_event, "")
    assert ret["statusCode"] == 200

```
pytestをインストールし、実行します。

```bash
$ pip install pytest pytest-mock --user
$ python -m pytest tests/ -v
tests/unit/test_handler.py::test_lambda_handler PASSED      [ 50%]
tests/unit/test_s3example.py::test_lambda_handler PASSED    [100%]
```
#####デプロイ
デプロイの準備を進めます。
複数の関数のデプロイを試したかったためこれまで触れていなかったHelloWorldFunctionも含まれます。

```Dockerfile:hello_world/Dockerfile
FROM public.ecr.aws/lambda/python:3.9
COPY app.py s3example.py requirements.txt ./
RUN python3.9 -m pip install -r requirements.txt -t .
```
```yaml:template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  python3.9

  Sample SAM Template for lambda-python3.9

# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
  Function:
    Timeout: 3

Resources:
  MyS3Bucket:
    Type: AWS::S3::Bucket
    
  HelloWorldFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      PackageType: Image
      #Handler: app.lambda_handler
      ImageConfig:
        Command:
          - "app.lambda_handler"
      Role: {先の手順で作成したLambda 実行ロールの ARN}
      Events:
        HelloWorld:
          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: /hello
            Method: get
    Metadata:
      Dockerfile: Dockerfile
      DockerContext: ./hello_world
      DockerTag: python3.9-v1

  S3exampleFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      PackageType: Image
      #Handler: s3example.lambda_handler
      ImageConfig:
        Command:
          - "s3example.lambda_handler"
      Role: {先の手順で作成したLambda 実行ロールの ARN}
      Events:
        S3exampleEvent:
          Type: S3
          Properties:
            Bucket: !Ref MyS3Bucket
            Events: s3:ObjectCreated:*
    Metadata:
      Dockerfile: Dockerfile
      DockerContext: ./hello_world
      DockerTag: python3.9-v1
```
S3イベントの指定の仕方について参考にさせていただきました。

https://qiita.com/Shi-nakaya/items/e2cfc23874b6623c3e8a

```bash:（samconfig.toml作成済み）
$ sam build
$ sam deploy
```
デプロイが完了したら動作確認をします。
コンソールからS3サービス画面に行き、作成されているS3バケットでファイルをアップロードします。
![スクリーンショット 2021-09-07 3.42.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/50efcc09-d137-420f-8528-7e4503089133.png)
書き込み用バケットにzipファイルが作成されました。
![スクリーンショット 2021-09-07 3.43.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/ec587aae-0717-26e0-8a7a-59791fb55a7f.png)

##おわりに
Lambda関数、慣れたら便利ですが、ログがわかりづらく慣れるまで戸惑いがありました。
config情報の環境変数化、レイヤーイメージやライブラリ同梱まで手が回りませんでした。
以下覚書です。

- `Command`を定義しないと2つ目の関数ハンドラが1つ目になる（関数名は2つ目だがハンドラがDockerfileのCMDにある1つ目になる）
- `Handler`は`Image`と併用できない
- `Bucket`は、同じ定義ファイル内のものを`!Ref`で呼ぶ必要がある
- `Role`は、指定しないと特に権限がない実行ロールが作成され関数に紐づく
- S3トリガーは、コンソール画面のLambdaからは見えないがS3の「イベント通知」でLambdaと紐づいているのが確認できる
- S3の「イベント通知」でコンソールから編集（何も変えずOK）するとLambda側のトリガーとして表示される
- 動作確認は、Lambdaの「テスト」か、本来のイベントを実行しCloudWatch log
- デプロイ時、おかしくなったらコンソールからCloudFormationの関連スタックを削除
- `template.yaml`を修正したら`sam build`からやり直す
- `boto3`は、イメージに最初から含まれているのでinstall不要。

お疲れ様でした。