httpでpostしたデータをmqttでpublishする(aws編その1)

# はじめに

[以前](https://qiita.com/pyjackal/items/6ffa1a094d6e29f933e6)、httpでpostされたデータをmqttでばらまく簡単なプログラムを書きました。この時はmosquitto(コンテナ) + python(flask + paho-mqtt)という構成で試しましたが、AWS IoT + labmda + api gatewayで書き直してみます。
AWS IoTはhttpプロトコルでメッセージをpublishできるのですが、今回の要件ではIP制限とbasic認証で接続を制限したかったので、labmda + api gatewayという構成にしてみました。

# 作業環境の前提条件

- aws cliがインストールされていること
- sam cliがインストールされていること

# 作成手順

## 1. samテンプレートのダウンロード

sam cliでテンプレートをダウンロードします

```bash
$ sam init
Which template source would you like to use?
        1 - AWS Quick Start Templates
        2 - Custom Template Location
Choice: 1
What package type would you like to use?
        1 - Zip (artifact is a zip uploaded to S3)
        2 - Image (artifact is an image uploaded to an ECR image repository)
Package type: 1

Which runtime would you like to use?
        1 - nodejs14.x
        2 - python3.9
        3 - ruby2.7
        4 - go1.x
        5 - java11
        6 - dotnetcore3.1
        7 - nodejs12.x
        8 - nodejs10.x
        9 - python3.8
        10 - python3.7
        11 - python3.6
        12 - python2.7
        13 - ruby2.5
        14 - java8.al2
        15 - java8
        16 - dotnetcore2.1
Runtime: 9

Project name [sam-app]: iottest

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

AWS quick start application templates:
        1 - Hello World Example
        2 - EventBridge Hello World
        3 - EventBridge App from scratch (100+ Event Schemas)
        4 - Step Functions Sample App (Stock Trader)
        5 - Elastic File System Sample App
Template selection: 1

    -----------------------
    Generating application:
    -----------------------
    Name: iottest
    Runtime: python3.8
    Architectures: x86_64
    Dependency Manager: pip
    Application Template: hello-world
    Output Directory: .

    Next application steps can be found in the README file at ./iottest/README.md


    Commands you can use next
    =========================
    [*] Create pipeline: cd iottest && sam pipeline init --bootstrap
    [*] Test Function in the Cloud: sam sync --stack-name {stack-name} --watch

$
```

私の環境がpython3.8なのでpython3.8を選択、Hello World ExampleではAPIを使用しているのHello World Exampleを選択します。実行すると、以下のようなディレクトリが生成されます。

```bash
$ ls -l iottest
total 28
drwxrwxr-x 2 ubuntu ubuntu 4096 Dec  8 07:39 events
drwxrwxr-x 2 ubuntu ubuntu 4096 Dec  8 07:32 hello_world
-rw-rw-r-- 1 ubuntu ubuntu    0 Dec  8 07:32 __init__.py
-rw-rw-r-- 1 ubuntu ubuntu 8349 Dec  8 07:32 README.md
-rw-rw-r-- 1 ubuntu ubuntu 1661 Dec  8 07:32 template.yaml
drwxrwxr-x 4 ubuntu ubuntu 4096 Dec  8 07:32 tests
$
```

これらファイルについては[こちら](https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/serverless-getting-started-hello-world.html)をご覧ください。

## 2. 生成されたファイルを変更する。

生成されたファイルを適宜変更します。

### 2-1. hello_worldのディレクトリ名変更

```bash
$ cd iotttest
$ mv hello_world publisher
```

### 2-2. app.pyの変更

```python:publisher/app.py
import json
import boto3

iot_client = boto3.client('iot-data')


def lambda_handler(event, context):
    try:
        body = json.loads(event["body"])
        if body and "topic" in body and 'payload' in body:
            iot_client.publish(
                topic=body["topic"],
                qos=0,
                payload=json.dumps(body["payload"]),
            )
        return _create_response(200, body)
    except Exception as e:
        raise e
    return _create_response(400, {"message": "Bad Request"})


def _create_response(statusCode, dictbody):
    return {
        "statusCode": statusCode,
        "body": json.dumps(dictbody),
    }
```

### 2-3. template.yamlの変更

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

  Sample SAM Template for iottest

Globals:
  Function:
    Timeout: 3

Resources:
  PublisherFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: publisher/  # パスをリネームしたディレクトリに変更
      Handler: app.lambda_handler
      Runtime: python3.8
      Architectures:
        - x86_64
      Events:
        Publisher:
          Type: Api
          Properties:
            Path: /publish  # 名前を変更
            Method: post  # postに変更
      Role: !GetAtt PublisherFunctionRole.Arn  # aws iotにpublishできるroleを指定

  PublisherFunctionRole:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          Effect: "Allow"
          Action: "sts:AssumeRole"
          Principal:
            Service:
              - lambda.amazonaws.com
      ManagedPolicyArns:
        - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
      Policies:
        -
          PolicyName: "IoTAllowPublishPolicy"
          PolicyDocument:
            Statement:
              -
                Effect: "Allow"
                Action: "iot:Publish"
                Resource: "*"
```

CloudWatchへのログアップロードを許可するAWSLambdaBasicExecutionRoleポリシーと、AWS IoTへのPublishを許可するインラインポリシー(IoTAlllowPublishPolicy)をアタッチしたPublisherFunctionRoleをlambda関数に割り当てる設定を追記しています。

## 3. アプリケーションのbuildとdeploy

本来はtestも修正すべきですが、今回は動作検証が目的なのでこのままdeployします

```bash
$ sam build
Building codeuri: /home/ubuntu/sam/iottest/publisher runtime: python3.8 metadata: {} architecture: x86_64 functions: ['PublisherFunction']
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
[*] Test Function in the Cloud: sam sync --stack-name {stack-name} --watch
[*] Deploy: sam deploy --guided

$ sam deploy --guided

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

        Looking for config file [samconfig.toml] :  Not found

        Setting default arguments for 'sam deploy'
        =========================================
        Stack Name [sam-app]: iottest
        AWS Region [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]:
        #Preserves the state of previously provisioned resources when an operation fails
        Disable rollback [y/N]:
        PublisherFunction may not have authorization defined, Is this okay? [y/N]: Y
        Save arguments to configuration file [Y/n]:
        SAM configuration file [samconfig.toml]:
        SAM configuration environment [default]:

        Looking for resources needed for deployment:
         Managed S3 bucket: aws-sam-cli-managed-default-samclisourcebucket-XXXXXXXXX
         A different default S3 bucket can be set in samconfig.toml

        Saved arguments to config file
        Running 'sam deploy' for future deployments will use the parameters saved above.
        The above parameters can be changed by modifying samconfig.toml
        Learn more about samconfig.toml syntax at
        https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-config.html

Uploading to iottest/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX  451163 / 451163  (100.00%)

        Deploying with following values
        ===============================
        Stack name                   : iottest
        Region                       : ap-northeast-1
        Confirm changeset            : True
        Disable rollback             : False
        Deployment s3 bucket         : aws-sam-cli-managed-default-samclisourcebucket-XXXXXXXX
        Capabilities                 : ["CAPABILITY_IAM"]
        Parameter overrides          : {}
        Signing Profiles             : {}

Initiating deployment
=====================
Uploading to iottest/XXXXXXXXXXXXXX.template  1246 / 1246  (100.00%)

Waiting for changeset to be created..

CloudFormation stack changeset
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Operation                                           LogicalResourceId                                   ResourceType                                        Replacement
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Add                                               PublisherFunctionPublisherPermissionProd            AWS::Lambda::Permission                             N/A
+ Add                                               PublisherFunctionRole                               AWS::IAM::Role                                      N/A
+ Add                                               PublisherFunction                                   AWS::Lambda::Function                               N/A
+ Add                                               ServerlessRestApiDeployment3eeb277620               AWS::ApiGateway::Deployment                         N/A
+ Add                                               ServerlessRestApiProdStage                          AWS::ApiGateway::Stage                              N/A
+ Add                                               ServerlessRestApi                                   AWS::ApiGateway::RestApi                            N/A
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

Changeset created successfully. arn:aws:cloudformation:ap-northeast-1:XXXXXXXXX:changeSet/samcli-deployXXXX/XXXX-XXXX-XXXX-XXXX-XXXXXXX


Previewing CloudFormation changeset before deployment
======================================================
Deploy this changeset? [y/N]: y

2021-12-08 08:08:48 - Waiting for stack create/update to complete

CloudFormation events from stack operations
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
ResourceStatus                                      ResourceType                                        LogicalResourceId                                   ResourceStatusReason
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
CREATE_IN_PROGRESS                                  AWS::IAM::Role                                      PublisherFunctionRole                               -
CREATE_IN_PROGRESS                                  AWS::IAM::Role                                      PublisherFunctionRole                               Resource creation Initiated
CREATE_COMPLETE                                     AWS::IAM::Role                                      PublisherFunctionRole                               -
CREATE_IN_PROGRESS                                  AWS::Lambda::Function                               PublisherFunction                                   -
CREATE_IN_PROGRESS                                  AWS::Lambda::Function                               PublisherFunction                                   Resource creation Initiated
CREATE_COMPLETE                                     AWS::Lambda::Function                               PublisherFunction                                   -
CREATE_IN_PROGRESS                                  AWS::ApiGateway::RestApi                            ServerlessRestApi                                   -
CREATE_IN_PROGRESS                                  AWS::ApiGateway::RestApi                            ServerlessRestApi                                   Resource creation Initiated
CREATE_COMPLETE                                     AWS::ApiGateway::RestApi                            ServerlessRestApi                                   -
CREATE_IN_PROGRESS                                  AWS::ApiGateway::Deployment                         ServerlessRestApiDeployment3eeb277620               -
CREATE_IN_PROGRESS                                  AWS::Lambda::Permission                             PublisherFunctionPublisherPermissionProd            Resource creation Initiated
CREATE_IN_PROGRESS                                  AWS::Lambda::Permission                             PublisherFunctionPublisherPermissionProd            -
CREATE_IN_PROGRESS                                  AWS::ApiGateway::Deployment                         ServerlessRestApiDeployment3eeb277620               Resource creation Initiated
CREATE_COMPLETE                                     AWS::ApiGateway::Deployment                         ServerlessRestApiDeployment3eeb277620               -
CREATE_IN_PROGRESS                                  AWS::ApiGateway::Stage                              ServerlessRestApiProdStage                          -
CREATE_IN_PROGRESS                                  AWS::ApiGateway::Stage                              ServerlessRestApiProdStage                          Resource creation Initiated
CREATE_COMPLETE                                     AWS::ApiGateway::Stage                              ServerlessRestApiProdStage                          -
CREATE_COMPLETE                                     AWS::Lambda::Permission                             PublisherFunctionPublisherPermissionProd            -
CREATE_COMPLETE                                     AWS::CloudFormation::Stack                          iottest                                             -
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

Successfully created/updated stack - iottest in ap-northeast-1

$
```

> PublisherFunction may not have authorization defined, Is this okay? [y/N]: Y

APIに認証がないので大丈夫かどうか聞かれていますが、動作検証なのでいったんこのまま進みます。

## 4. テストの実行

AWS IoTのMQTTテストクライアントを使ってAWS IoTにpublishされているか確認します。

### 4-1. MQTTテストクライアントでdescribe

aws consoleでAWS IoT > MQTT test clientページを開き、test/topicをdescribeします。

![mqtt-test-client.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2109677/f81f0abb-2671-4f46-f622-b72c9f63dbbb.jpeg)

### 4-2. テスト用イベントデータ作成

AWS Lambda > アプリケーション > iottestページを開き、PublisherFunctionをクリックします。
lambdaファンクションのページが開いたら、テストタブを選択し、テストイベントを作成に取り掛かります。
テンプレートにapigateway-aws-proxyを選ぶとapigateway用のテストイベントテンプレートが表示されるので、bodyのデータを

```yaml
"body": "{\"topic\": \"test/topic\",\"payload\": {\"message\": \"hello everybody\"}}",
```

に書き換えます。書き換え後、わかりやすい名前を付けて変更を保存を押します。

![testdata.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2109677/7f36fb0c-4611-3a22-7166-1f77264954aa.jpeg)

### 4-3. テストの実行と確認

テスト実行します。うまくいくと、以下のようなテストログが表示され、AWS IoTのテストクライアントでメッセージを受け取れます。

![result1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2109677/d801763d-9136-cb3b-d2ce-4e807f6f586f.jpeg)

![result2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2109677/6e22ebb4-f22a-c29f-0d45-0b11623f95c7.jpeg)

##### *エラーが出る場合

実行時、タイムアウトでエラーとなることがあります。

![error.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2109677/9ddd75e3-c581-6c3d-b67d-c86e8272d075.jpeg)

設定からタイムアウトの時間を伸ばして実行すると、タイムアウトの原因がわかることがあります。

![error2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2109677/47d16e76-7e22-9d7c-3548-ec16e8d0719f.jpeg)

この、certificate verify failedですが、[こちらの記事](https://dev.classmethod.jp/articles/lambda-boto3-ssl-error-custom-hotfix/)によると、certifiのバージョンの問題のようです。たしかに、エラーとなるときにbuildされた結果を見ると、certifiのバージョンが2021.10.8になっています。

```bash
$ ls -l .aws-sam/build/PublisherFunction
total 48
-rw-rw-r-- 1 ubuntu ubuntu  654 Dec  9 21:32 app.py
drwxrwxr-x 2 ubuntu ubuntu 4096 Dec 10 00:44 certifi
drwxrwxr-x 2 ubuntu ubuntu 4096 Dec 10 00:44 certifi-2021.10.8.dist-info
drwxrwxr-x 4 ubuntu ubuntu 4096 Dec 10 00:44 charset_normalizer
drwxrwxr-x 2 ubuntu ubuntu 4096 Dec 10 00:44 charset_normalizer-2.0.9.dist-info
drwxrwxr-x 2 ubuntu ubuntu 4096 Dec 10 00:44 idna
drwxrwxr-x 2 ubuntu ubuntu 4096 Dec 10 00:44 idna-3.3.dist-info
-rw-rw-r-- 1 ubuntu ubuntu    0 Dec  9 21:30 __init__.py
drwxrwxr-x 2 ubuntu ubuntu 4096 Dec 10 00:44 requests
drwxrwxr-x 2 ubuntu ubuntu 4096 Dec 10 00:44 requests-2.26.0.dist-info
-rw-rw-r-- 1 ubuntu ubuntu    9 Dec 10 00:44 requirements.txt
drwxrwxr-x 5 ubuntu ubuntu 4096 Dec 10 00:44 urllib3
drwxrwxr-x 2 ubuntu ubuntu 4096 Dec 10 00:44 urllib3-1.26.7.dist-info
$
```

これは、テンプレートで生成されたrequirements.txtにrequestsが記載されていることから、関連パッケージとしてcertifiがインストールされていることによるものです。
今回のコードではrequestsを使用していないのでrequirements.txtからrequestsを消すとエラーが解消されます。
もし、requestsを使うコードを書いている場合は、requirements.txtにcertifi==2020.11.8を記載するとうまくいくと思います。

### 4-4. APIからのテスト

AWS Lambda > アプリケーション > iottestページを開き、ServerlessRestApi をクリックします。
/publishのPOSTメソッドテストを以下のようなリクエスト本文で実行します。

```text
{"topic": "test/topic", "payload": {"message": "hello everyone"}}
```

![request.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2109677/4d0d0bea-0f8b-b196-7a47-7ed19f0ec8e3.jpeg)

うまくいくと、以下のようなレスポンス本文が表示され、AWS IoTのテストクライアントのほうでもメッセージが受け取れます。

![result3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2109677/4b1215c7-c201-a083-6706-a389585c3217.jpeg)

![result4.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2109677/6e23c083-99c0-caf5-b304-34476d16be4b.jpeg)

### 4-5. 実際にpostしてテスト

現時点ではAPI Gatewayに制限がかかっていないので、curlでデータをpostして動作確認します。

```bash
$ curl -X POST -d '{"topic": "test/topic", "payload": {"messege": "hello world"}}' https://zqw1v6w7oc.execute-api.ap-northeast-1.amazonaws.com/Prod/publish && echo
{"topic": "test/topic", "payload": {"messege": "hello world"}}
$
```

コマンド実行後に改行されないケースは&& echoを付けると改行されるのでちょっぴり幸せです。

![result5.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2109677/88483696-5f2b-292e-e492-37ba64ee1fd8.jpeg)

長くなったのでここまで。次回はモノとAPIへのIP制限を追加します。