SQSでCognitoユーザー一覧をS3に作成するキュー処理を行う関数とAPIをLambdaにSAMでデプロイする


日本語が不自由なタイトルになりましたが順に設定していきます。

AWSのCognitoで管理されているユーザー一覧のCSVファイルをS3上に自動で作成する関数をLambda上に構築します。
そのLambdaを発動させるトリガーにSQSを設定し、SQSにメッセージキューを追加するAPIも別のLambdaに実装しAPI Gateway経由でHTTPリクエストを受け取るようにします。

* Cognito: ユーザー管理サービス。多要素認証(MFA)やSNS連携などを簡単に実装できる。
* S3: クラウドストレージ。バージョニングや一時的に利用可能なダウンロードURL作成などもできる。
* SQS: AWS上でキューを管理する。非同期処理などに利用される。
* Lambda: サーバーレスの関数実行環境
* API Gateway: LambdaやEC2にAPIのインターフェースを提供する。

構成図は以下のとおりです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74622/36723717-c969-cbe0-73e3-4b139d0c1c44.png)



### AWS-CLIのインストールとIAMの設定

```
$ brew install awscli
$ aws configure
AWS Access Key ID [None]: ************
AWS Secret Access Key [None]: ************************
Default region name [None]: ap-northeast-1
Default output format [None]: json
```

### SAMのインストール
※ Dockerも必要になるためインストールと起動をしてください。

```
$ brew tap aws/tap
$ brew install aws-sam-cli
$ sam --version
SAM CLI, version 1.0.0
```

### SAMプロジェクトの初期化
今回はPython3.8でHello Worldテンプレートをベースに実装を進めます

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

Which runtime would you like to use?
	1 - nodejs12.x
	2 - python3.8
	3 - ruby2.7
	・・・
Runtime: 2

Project name [sam-app]: sample-app

Cloning app templates from https://github.com/awslabs/aws-sam-cli-app-templates.git

AWS quick start application templates:
	1 - Hello World Example
	2 - EventBridge Hello World
	・・・
Template selection: 1

-----------------------
Generating application:
-----------------------
Name: sample-app
Runtime: python3.8
Dependency Manager: pip
Application Template: hello-world
Output Directory: .

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

### Cognitoのユーザープールの作成

今回はひとまずデフォルト設定で作成して、適当なユーザーを一つ手動で追加しました。
作成したユーザープールIDをファイル作成時に利用します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74622/41c06149-59fb-bda8-7ddc-49b1639a277f.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74622/33dac72c-0e8e-25f7-a540-17ab3737dd0b.png)

### 実装
主要なファイルを以下のように実装しました。

``` python:app.py
import json
import requests
import boto3
from datetime import datetime
import pandas as pd


def lambda_handler(event, context):

    try:
        sqs_client = boto3.client("sqs")
        # 一度SQSをデプロイすると自動でSQSにキューが作成されるのでそれを設定します
        queue_url = "https://sqs.ap-northeast-1.amazonaws.com/********/**********"
        print(queue_url)
        now = datetime.now()
        date_str = now.strftime('%Y/%m/%d-%H:%M:%S')
        sqs_response = sqs_client.send_message(
            QueueUrl=queue_url,
            MessageBody=json.dumps(date_str)
        )
    except requests.RequestException as e:
        # Send some context about this error to Lambda Logs
        print(e)

        raise e

    return {
        "statusCode": 200,
        "body": json.dumps({
            "message": "hello world"
        }),
    }

def csv_create_handler(event, context):
    for record in event['Records']:
        payload=record["body"]

        date_str = str(payload)
        s3 = boto3.resource('s3')
        bucket = s3.Bucket('arkth-user-list-files')

        cognito_client = boto3.client('cognito-idp')

        response = cognito_client.list_users(
            UserPoolId = 'ap-northeast-***********',
            AttributesToGet = ['email','sub']
        )

        data = []
        for user in response["Users"]:
            data.append([user['Username'], user['Enabled']])

        Coulum = ['Username', 'Enabled']
        df = pd.DataFrame(data, columns=Coulum)

        df_csv = df.to_csv(index=None)
        objkey = 'user-list.csv'
        print(objkey)
        putobj = bucket.Object(objkey)
        putobj.put(Body=df_csv)

def support_datetime_default(obj):
    if isinstance(obj, datetime):
        return obj.isoformat()
    raise TypeError(repr(obj) + " is not JSON serializable")
```


```yaml:template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  sample-app

  Sample SAM Template for sample-app

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

Resources:
  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:
      CodeUri: hello_world/
      Handler: app.lambda_handler
      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

  UsersCsv:
    Type: AWS::SQS::Queue
    Properties:
      VisibilityTimeout: 60

  CsvCreateFunction:
    Properties:
      CodeUri: hello_world/
      Handler: app.csv_create_handler
      Events:
        MySQSEvent:
          Type: SQS
          Properties:
            Queue: !GetAtt UsersCsv.Arn
            BatchSize: 10
    Type: AWS::Serverless::Function
```

```requirement.txt
requests
boto3
pandas
```

### SAMのビルド

```
$ sam build
Building function 'HelloWorldFunction'
Running PythonPipBuilder:ResolveDependencies
Running PythonPipBuilder:CopySource
Building function 'CsvCreateFunction'
Running PythonPipBuilder:ResolveDependencies
Running PythonPipBuilder:CopySource

Build Succeeded

Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml

Commands you can use next
=========================
[*] Invoke Function: sam local invoke
[*] Deploy: sam deploy --guided
```

### ローカルでSAMの起動

```
$ sam local start-api
Mounting HelloWorldFunction at http://127.0.0.1:3000/hello [GET]
You can now browse to the above endpoints to invoke your functions. You do not need to restart/reload SAM CLI while working on your functions, changes will be reflected instantly/automatically. You only need to restart SAM CLI if you update your AWS SAM template
2020-09-05 22:11:22  * Running on http://127.0.0.1:3000/ (Press CTRL+C to quit)
```

SQSのエンドポイントがダミーのためエラーになりますが、ブラウザから `http://127.0.0.1:3000/hello` にアクセスしてSyntax Errorなどがないか確かめます。

### SAMのデプロイ
初回は --guided でデプロイすることで対話式で、samconfig.tomlが作成されます。

```
$ sam deploy --guided

Configuring SAM deploy
======================

	Looking for samconfig.toml :  Not found

	Setting default arguments for 'sam deploy'
	=========================================
	Stack Name [sam-app]: sample-app
	AWS Region [us-east-1]: ap-northeast-1
	#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
	HelloWorldFunction may not have authorization defined, Is this okay? [y/N]: y
	Save arguments to samconfig.toml [Y/n]: Y
・・・・
・・・・
・・・・
```

少し時間がかかりますが、IAMやApi Gatewayなども適当に設定してデプロイしてくれます。

### 環境設定
* app.py内のSQSのエンドポイントを、作成されたSQSのエンドポイントに書き換えます。
* ファイル作成のLambda関数に割りてられているIAMのRoleにSQSとS3にアクセスする権限を付与します。

* 以下のように、APIの方のLambda関数のDestinationに作成されたSQSを設定します。ここはSAMで設定できるのかもしれないのですが、今回は手動で行いました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74622/4cfac618-648f-efcf-50eb-970dbec6ed5d.png)

その後、再度build & deployします。

```
$ sam build
$ sam deploy
```

### 動作確認
HelloWorldの方のLambda関数に設定されているAPI Gatewayのエンドポイントにブラウザでアクセスします。

その後S3のBucketにアクセスすると、以下のようなCSVファイルが作成されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74622/ffc2a198-c330-2aaf-a44b-157814064633.png)