ServerlessFrameworkとPythonでAWSを触ってみる


# はじめに

12月も早いもので中盤を迎えましたね。個人的なお話になりますがこの日はお誕生日なのでとってもハッピーな気分でいます。この記事もそんな気持ちで書いています。
最近、ServerlessFrameworkとAWSを触っているのでそれに関して自分なりにまとめてみました。使用したサービスは、LambdaとS3、SQS、API Gatewayなのでそのことについて書いてあります。

今回使用したコードは[ここ](https://github.com/masaemon/aws-serverless)にあります。


また、本記事を作成するにあたり以下の記事を参考にしました。
[とことんサーバーレス①：Serverless Framework入門編](https://qiita.com/hiroshik1985/items/6d979ff1afb56953b62e)
[Serverless FrameworkとS3で超簡単な投票システムを作った話](https://qiita.com/is_ryo/items/ffe59a06df500d530d14)
[Serverless FrameworkでLambdaをPython 3.6で使ってみた]
(https://dev.classmethod.jp/cloud/aws/serverless-framework-with-python-3-6/)
[Serverless Frameworkの使い方まとめ](https://qiita.com/horike37/items/b295a91908fcfd4033a2)

## 準備・環境
Node.jsとPythonの環境が必要です。あとは、AWSのアカウントとかも必要になります。
Node.jsはServerlessFramework、PythonはAWS CLIを利用するために必要です。

## 1. Serverlessのインストール
npmでインストールします。

```
$ npm install -g serverless
```

```
＄ sls -v
Framework Core: 1.54.0
Plugin: 3.1.2
SDK: 2.1.2
Components Core: 1.1.1
Components CLI: 1.4.0
```
このように表示されればインストール完了です。

## 2. IAMユーザーの作成とAWS Credentialを設定する
ServerlessFrameworkでAWSを利用するためのIAMを作成する必要があります。
### IAMユーザーの作成
IAMのダッシュボードでユーザーからユーザーの作成をクリックすることで作成できます。基本的には、プログラムによるアクセスにチェックを入れることと今回、作成するのに必要な権限を与えれば大丈夫です。また、AWS Credentialのキーはここでしか取得できないので注意します。

### AWS Credentialの設定
AWS Credentialの設定を行うためには、AWS CLIをインストールする必要があります。以下のコマンドでインストールできます。

```
$ pip install aws-sdk
```

```
$ aws --version
aws-cli/1.16.276 Python/3.7.3 Darwin/18.7.0 botocore/1.13.12
```
このように表示されればインストールが完了しています。
以下のようにしてAWSCredentialの設定を行います。

```
$ aws configure
AWS Access Key ID [None]: 作成したIAMのアクセスキーID
AWS Secret Access Key [None]: 作成したIAMのシークレットアクセスキー
Default region name [None]: ap-northeast-1
Default output format [None]: ENTER
```

引用: [とことんサーバーレス①：Serverless Framework入門編](https://qiita.com/hiroshik1985/items/6d979ff1afb56953b62e)

これでServerlessFrameworkを利用する準備は完了です。

## 3. ServerlessFrameworkでHelloWorld
まずは、Serverlessのプロジェクトを作成していきます。
以下のコマンドを打つことで作成できます。

```
$ sls create -t aws-python3 -p ほげほげぷろじぇくと
```

以下のように表示されれば成功です。

```
Serverless: Generating boilerplate...
Serverless: Generating boilerplate in "/..."
 _______                             __
|   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
|   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
|____   |_____|__|  \___/|_____|__| |__|_____|_____|_____|
|   |   |             The Serverless Application Framework
|       |                           serverless.com, v1.54.0
 -------'

Serverless: Successfully generated boilerplate for template: "aws-python3"
```

次に、プロジェクト名のディレクトリにある```handler.py```を以下のようにします。

```python:handler.py
import json

def hello(event, context):
    body = {
        "message": "Hello, world",
    }

    response = {
        "statusCode": 200,
        "body": json.dumps(body)
    }

    return response

```

また、デプロイ先のリージョンを東京にします。

```yaml:serverless.yml
service: ほげほげ

provider:
  name: aws
  runtime: python3.7
  region: ap-northeast-1 #東京リージョンにデプロイする

functions:
  hello:
    handler: handler.hello

```

そして、デプロイをします。

```
$ sls deploy
```

実行します。

```
＄ sls invoke -f hello
```

以下のように表示されれば成功です。

```
{
    "statusCode": 200,
    "body": "{\"message\": \"Hello, world\"}"
}
```

また、AWSのLambdaのダッシュボードにデプロイした関数があるので、そこから実行してみても構いません。

## 4. API GateWayからLambdaを起動する

まず、先ほどデプロイしたhello関数をAPI Gatewayを利用して実行してみます。```serverless.yml```を以下のように変更します。

```yaml:serverless.yml
service: ほげほげ

provider:
 ...

functions:
  hello:
    handler: handler.hello
    # ここから追加
    events:
      - http:
          path: hello
          method: get
          cors: true
```
引用: https://serverless.com/framework/docs/providers/aws/events/apigateway/

このようにしてから```sls deploy```をすると以下のように```endpoints```がでます。

```
Service Information
...
endpoints:
  GET - https://hogehoge.amazonaws.com/dev/hello
functions:
  hello: hogehoge-dev-hello
layers:
  None
```

このAPIをcurlコマンドなどで叩くと、HelloWorldが表示されます。

## 5. S3にファイルを保存してそれを読み取る

### S3にファイルを保存する関数の作成
新たに、```sthreeput.py```を作成し、以下のように記述します。
ここでは、100までのランダムな数字を10個記述してあるテキストファイルを日付と時刻の名前で保存する関数を作成します。

```python:sthreeput.py
import boto3 #Python用のAWS SDK

import datetime
import random

s3 = boto3.resource('s3')
bucketName = "hogehogebucket" # 自分で決めたS3バケットの名前

def index(event, context):
    dt = datetime.datetime.now()
    text = ""
    for i in range(10):
        num = str(random.randint(0, 100))
        text += num + " "

    key = "{0:%Y-%m-%d-%H-%M-%S}.txt".format(dt) #保存するファイル名になる

    obj = s3.Object(bucketName, key) 
    obj.put(Body=text.encode())

    return {
        "statusCode": 200,
        "body": text
    }
```


### serverless.ymlの設定
ファイルを保存するようのS3バケットを作成します。また、S3にファイルを追加するための関数についても記述します。S3のバケット名は、すでにあるもの（自分以外のものでも）だと使えないので気をつけてください。

```yaml:serverless.yml

functions:
  hello:
  ....
  #関数名
  sthree-put:
    handler: sthreeput.index #sthreeput.pyのindex関数を起動する

resources:
  Resources:
    SthreeBucket: # S3のバケットを作成する
      Type: AWS::S3::Bucket
      Properties:
        BucketName : "hogehogebucket" # S3のバケットの名前（すでにある名前はつけられない）
```

これで```sls deploy```して、関数を実行してみましょう。うまくいっていれば、作成したバケットにファイルが保存されています。

### S3にファイルが保存されたのをトリガーにしてファイルを見る
新たに```sthreereceive.py```を以下のように作成します。

```python:sthreereceive.py
import boto3

s3 = boto3.client('s3')

def index(event, context):
    bucket = event["Records"][0]["s3"]["bucket"]["name"] #バケットの名前
    key = event["Records"][0]["s3"]["object"]["key"] #保存したファイルの名前
    response = s3.get_object(Bucket=bucket, Key=key) #バケットの中にあるオブジェクトの取得
    body = response['Body'].read().decode("utf-8") #ファイルの中の情報を取得してutf-8にデコードする
    print(body)
    return body
```

S3にフックされてLambda関数が起動すると1番目の引数eventに以下のようなJSONが渡されます。

```json
{
  "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": "example-bucket",
          "ownerIdentity": {
            "principalId": "EXAMPLE"
          },
          "arn": "arn:aws:s3:::example-bucket"
        },
        "object": {
          "key": "test/key", 
          "size": 1024,
          "eTag": "0123456789abcdef0123456789abcdef",
          "sequencer": "0A1B2C3D4E5F678901"
        }
      }
    }
  ]
}
```

この情報からS3バケットの名前とファイル名であるkeyを取得します。

### serverless.ymlの設定
関数の設定とS3のイベントの設定を行います。

```yaml:serverless.yml
...
functions:
  sthree-put:
    handler: sthreeput.index #sthreeput.pyのindex関数を起動する

  sthree-receive:
    handler: sthreereceive.index
    events:
    - s3:
        bucket: "sthreebucket-hogehoge" #バケットの名前
        event: s3:ObjectCreated:* #S3に作られた時に起動
        existing: true #S3のバケットを作成しているためこれをつける
```

最後の```existing: ture```がない場合、バケットが作成されるためすでに同じ名前のバケットがある場合デプロイすることができません。先ほど、バケットを作成したためこのオプションを設定します。serverless.ymlのResourcesの部分を消し、```sls remove```をしてまっさらな状態にすれば、最後のオプションがなくてもデプロイできます。

さて、これで```sls deploy```を行い、sthreeput関数を起動すれば、バケットにファイルが保存され、sthreereceive関数が起動していることがLambdaのダッシュボードのモニタリングから確認できます。

また、今回はS3に保存されたことをトリガーにして、ファイルの確認を行いましたが、バケットの名前と確認したいファイルのキーを指定してファイルの確認をすることもできます。

## 6. SQSにメッセージを送信してそのメッセージ受け取る

### SQSにメッセージを送る関数の作成
新たに```sendmessage.py```を以下のように記述します。

```python:sendmessage.py
import boto3
import random
import json

sqs = boto3.resource('sqs')
queueName = "hogehoge-queue"


def index(event, context):
    queue = sqs.get_queue_by_name(QueueName=queueName)  # キューを取得

    message = ""  # 送信するメッセージ

    for i in range(10):
        num = str(random.randint(0, 100))
        message += num + " "
        
    print(message)

    queue.send_message(MessageBody=json.dumps({ "message": message })) # メッセージの送信
    
    return message

```

### serverless.ymlの設定
メッセージを送信するためのキューを作成します。S3のバケットとは違いSQSのキューの名前は、他のアカウントの人と名前が同じでも大丈夫です。

```yaml:serverless.yml
provider:
  ...
  iamRoleStatements:
  - Effect: "Allow"
    Action:
    - "s3:PutObject"
    - "s3:GetObject"
    - "s3:ListBucket"
    - "sqs:CreateQueue"
    - "sqs:DeleteQueue"
    - "sqs:SendMessage"
    - "sqs:GetQueueUrl"
    Resource: "*"
...
functions:
  sthree-put:
  ...
  sendmessage:
    handler: sendmessage.index

resources:
  Resources:
    SthreeBucket: 
    ...
    hogehogeQueue:
      Type: AWS::SQS::Queue # SQSのキュー作成
      Properties:
        QueueName: "hogehoge-queue"
```

これで```sls deploy```を行い、sendmessage関数を起動すれば、作成したキューにメッセージが追加されているはずです。

### SQSにメッセージを送るのトリガーにメッセージを受け取る

次に、先ほど送信したメッセージを受け取ってみましょう。```receivemessage.py```を作成して以下のように記述します。

```python:receivemessage.py
import json

def index(event, context):
    body = json.loads(event["Records"][0]["body"]) 
    message = body["message"]
    print(message)
    return message
```

SQSにフックされてLambda関数が起動すると1番目の引数eventに以下のようなJSONが渡されます。

```json
{
  "Records": [
    {
      "messageId": "19dd0b57-b21e-4ac1-bd88-01bbb068cb78",
      "receiptHandle": "MessageReceiptHandle",
      "body": "Hello from SQS!", 
      "attributes": {
        "ApproximateReceiveCount": "1",
        "SentTimestamp": "1523232000000",
        "SenderId": "123456789012",
        "ApproximateFirstReceiveTimestamp": "1523232000001"
      },
      "messageAttributes": {},
      "md5OfBody": "7b270e59b47ff90a553787216d55d91d",
      "eventSource": "aws:sqs",
      "eventSourceARN": "arn:aws:sqs:ap-northeast-1:123456789012:MyQueue",
      "awsRegion": "ap-northeast-1"
    }
  ]
}
```

ここからbodyを取得することによって、送信したメッセージを取得できます。

### serverless.ymlの設定
```yaml:serverlss.yml
...
functions:
  receivemessage:
    handler: receivemessage.index
    events:
      - sqs:
          # arn:aws:sqs:region:accountid:hogehoge-queue みたいになる
          arn:
            Fn::Join:
            - ':'
            - - arn
              - aws
              - sqs
              - Ref: AWS::Region
              - Ref: AWS::AccountId
              - "hogehoge-queue"
```

これで```sls deploy```をしてsendmessage関数を起動するとreceivemessage関数が起動します。receivemessage関数がエラーを起こしているとなんども起動されるので注意しましょう。この場合、キューのメッセージをクリアすることによって止めることができます。

# おわりに
必要なければ削除しましょう。
以下のコマンドできれいさっぱりになります。

```
sls remove
```

バケットが上手く消えなかったりするのですがその時は、バケットを空にしたり、直接スタックを削除すると良いかもしれません。

AWSはいろんなサービスが他にもあるのでと触ってみようかなぁと思いました