Amazon ConnectからAWS Lambdaを呼び出し、SMS送信してみた

2024.06.28

はじめに

Amazon ConnectからAWS Lambdaを呼出し、SMS送信する方法を紹介します。

発信元の電話番号がSMS送信可能な番号であれば送信し、その番号がSMS送信できない場合は、手動でSMS送信先の電話番号を入力してもらう仕組みを実装しました。

前提条件

Lambda 関数作成

SMS送信用のLambda 関数を作成します。

SMS送信を許可するIAMポリシーを作成し、Lambda関数のIAMロールに適用します。

ポリシーは以下の通りです。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                 "sns:Publish"
            ],
            "NotResource": "arn:aws:sns:*:*:*"
        }
    ]
}

NotResourceを指定することで、トピックに対する実行権限を付与することなく、携帯電話番号へのSMS送信が可能になります。参考記事

コードは以下の通りです。

import boto3
import json

def lambda_handler(event, context):
    print('event:' + json.dumps(event, ensure_ascii=False))

    phone_number = get_phone_number(event)
    message = create_message()

    try:
        send_sms(phone_number, message)
        return {}
    except Exception as e:
        print(f'SMSの送信に失敗しました。エラー: {str(e)}')

def get_phone_number(event):
    if 'input_phone_number' in event['Details']['ContactData']['Attributes']:
        input_phone_number = event['Details']['ContactData']['Attributes']['input_phone_number']
        return '+81' + input_phone_number[1:]  # 090xxxxxxxx -> +8190xxxxxxxx
    else:
        return event['Details']['ContactData']['CustomerEndpoint']['Address']

def create_message():
    body = [
        'これはAWS LambdaからのSMSテストメッセージです。',
        'このメッセージは3行に分かれています。',
        'これが最後の行です。'
    ]
    return '\n'.join(body)

def send_sms(phone_number, message):
    sns = boto3.client('sns')
    sender_id = 'classmethod'  # 送信者ID

    response = sns.publish(
        PhoneNumber=phone_number,
        Message=message,
        MessageAttributes={
            'AWS.SNS.SMS.SenderID': {
                'DataType': 'String',
                'StringValue': sender_id
            }
        }
    )
    print(f'SMSが正常に送信されました。メッセージID: {response["MessageId"]}')

実装ポイント

get_phone_number

Amazon ConnectのフローからLambda関数に渡される電話番号の形式は、2パターンあることに注意ください。

Lambdaに渡されるevent内容は、以下の2パターンです。

  1. 発信元の電話番号にSMSを送信できる場合、発信元の電話番号にSMS送信します
  2. 発信元の電話番号がSMS送信できない場合、手動でSMS送信先の電話番号を入力し、SMS送信します。

発信元番号は「+81xxx」形式ですが、ユーザー入力の番号は「0xxx」形式となります。

ユーザー入力の番号は「0xxx」形式は「+81xxx」に変換処理を行うことで、どちらの番号形式にも対応できるようにしています。

発信元の電話番号にSMSを送信できる場合、以下のeventが渡されます。Attributesは空で、CustomerEndpoint内の発信元電話番号を利用します。

{
    "Details": {
        "ContactData": {
            "Attributes": {},
            "CustomerEndpoint": {
                "Address": "+8190xxxxxxxx",
                "Type": "TELEPHONE_NUMBER"
            },
        ~省略~

発信元の電話番号がSMS送信できない場合、手動でSMS送信先の電話番号を入力し、以下のeventが渡されます。Attributes内の手動で入力した電話番号を利用します。

{
    "Details": {
        "ContactData": {
            "Attributes": {
                "input_phone_number": "090xxxxxxxx"
            },
            "CustomerEndpoint": {
                "Address": "+8150xxxxxxxx",
                "Type": "TELEPHONE_NUMBER"
            },
        ~省略~

return

Lambdaの戻り値を使い分けることで、Connectフローの遷移先を制御できます。

SMS送信成功時は空のオブジェクトを返して「成功」に遷移し、エラー時は何も返さずに「エラー」に遷移するようにしています。

これにより、実行結果に応じた適切なフロー制御が可能になります。

SMS送信の制限

SMSの送信者を識別する送信者IDは、3文字以上11文字以内という制限があります。

[送信者 ID] に、少なくとも 1 つの文字を含み、スペースは含まない、3~11 文字の英数字のカスタム ID を入力します。 引用ドキュメント

送信されるメッセージは140バイトで分割されて送信されますので注意してください。

ただし、メッセージが140バイトを超える、例えば200バイトの場合、2つに分割されて受信されるか、分割されずに1つのメッセージとして受信されるかは、携帯電話会社や端末によって異なります。

各 SMS メッセージは最大 140 バイトまで含めることができ、文字限度はエンコーディングスキームによって異なります。例えば、SMS メッセージには以下を含めることができます。 160 GSM 文字 140 ASCII 文字 70 UCS-2 文字

Connectフロー

Connectフローは以下の通りです。

フローコード (クリックすると展開します)
{
  "Version": "2019-10-30",
  "StartAction": "00e56ecd-f363-4a8b-b70b-e17616278f40",
  "Metadata": {
    "entryPointPosition": { "x": 128.8, "y": 96.8 },
    "ActionMetadata": {
      "4ff267b8-6fa5-4256-ac19-6d0703fd81fc": {
        "position": { "x": 131.2, "y": 248.8 },
        "conditionMetadata": [
          {
            "id": "e89535f6-c067-42a3-bcbf-2155ddf734e4",
            "operator": {
              "name": "Starts with",
              "value": "StartsWith",
              "shortDisplay": "starts with"
            },
            "value": "+8170"
          },
          {
            "id": "4b157c54-9d09-4695-a686-3d7ecb50fbe3",
            "operator": {
              "name": "Starts with",
              "value": "StartsWith",
              "shortDisplay": "starts with"
            },
            "value": "+8180"
          },
          {
            "id": "7fb14539-4610-4133-bcfa-301d39218c79",
            "operator": {
              "name": "Starts with",
              "value": "StartsWith",
              "shortDisplay": "starts with"
            },
            "value": "+8190"
          }
        ]
      },
      "00e56ecd-f363-4a8b-b70b-e17616278f40": {
        "position": { "x": 232.8, "y": 76 }
      },
      "564ac430-c916-4279-8907-60f96080b1a3": {
        "position": { "x": 450.4, "y": 76.8 },
        "children": ["cbbace15-32aa-4949-b88b-90cc5bf21a1e"],
        "overrideConsoleVoice": true,
        "fragments": {
          "SetContactData": "cbbace15-32aa-4949-b88b-90cc5bf21a1e"
        },
        "overrideLanguageAttribute": true
      },
      "cbbace15-32aa-4949-b88b-90cc5bf21a1e": {
        "position": { "x": 450.4, "y": 76.8 },
        "dynamicParams": []
      },
      "90c242e6-360d-4be9-8cfa-645b33ac6a84": {
        "position": { "x": 565.6, "y": 482.4 },
        "parameters": {
          "Attributes": { "input_phone_number": { "useDynamic": true } }
        },
        "dynamicParams": ["input_phone_number"]
      },
      "d1730443-5dea-4b16-a33a-fb8e25c4b607": {
        "position": { "x": 1471.2, "y": 289.6 }
      },
      "9b196cfd-e7c7-4c60-9897-87390d7df8cc": {
        "position": { "x": 1028.8, "y": 796.8 }
      },
      "2906039d-70d3-416a-ba18-0b60b178e6ec": {
        "position": { "x": 1706.4, "y": 848.8 }
      },
      "d5e69705-2a25-4beb-9e0e-a19c1218ae62": {
        "position": { "x": 808, "y": 480.8 },
        "conditionMetadata": [
          {
            "id": "8009a7a5-5f3e-40ab-be62-962223a6ca8a",
            "operator": {
              "name": "Starts with",
              "value": "StartsWith",
              "shortDisplay": "starts with"
            },
            "value": "070"
          },
          {
            "id": "e674c1fb-5e94-4c95-92d9-4f81c73686c9",
            "operator": {
              "name": "Starts with",
              "value": "StartsWith",
              "shortDisplay": "starts with"
            },
            "value": "080"
          },
          {
            "id": "71408375-01ee-469f-bf63-e35e6f33643c",
            "operator": {
              "name": "Starts with",
              "value": "StartsWith",
              "shortDisplay": "starts with"
            },
            "value": "090"
          }
        ]
      },
      "e5ae829a-953e-4490-a846-c49518fa0bb3": {
        "position": { "x": 1257.6, "y": 288.8 },
        "parameters": {
          "LambdaFunctionARN": { "displayName": "cm-hirai-send_sms" }
        },
        "dynamicMetadata": {}
      },
      "52578cfa-32ab-4dc5-ae78-0829748b80e7": {
        "position": { "x": 341.6, "y": 486.4 },
        "conditionMetadata": [],
        "countryCodePrefix": "+1"
      },
      "15562c61-adf4-4aeb-a6a6-b5a0282aae7f": {
        "position": { "x": 1475.2, "y": 708.8 }
      },
      "2c2c44f1-1d41-487a-9e42-00700771103c": {
        "position": { "x": 1029.6, "y": 480 },
        "conditionMetadata": [
          { "id": "43f09e33-0220-4cf7-b689-d983ce737b29", "value": "1" },
          { "id": "c0dcbab6-e99b-4981-b44f-f84b77eb984f", "value": "2" }
        ]
      }
    },
    "Annotations": [],
    "name": "cm-hirai-send-sms",
    "description": "",
    "type": "contactFlow",
    "status": "published",
    "hash": {}
  },
  "Actions": [
    {
      "Parameters": { "ComparisonValue": "$.CustomerEndpoint.Address" },
      "Identifier": "4ff267b8-6fa5-4256-ac19-6d0703fd81fc",
      "Type": "Compare",
      "Transitions": {
        "NextAction": "52578cfa-32ab-4dc5-ae78-0829748b80e7",
        "Conditions": [
          {
            "NextAction": "e5ae829a-953e-4490-a846-c49518fa0bb3",
            "Condition": { "Operator": "TextStartsWith", "Operands": ["+8170"] }
          },
          {
            "NextAction": "e5ae829a-953e-4490-a846-c49518fa0bb3",
            "Condition": { "Operator": "TextStartsWith", "Operands": ["+8180"] }
          },
          {
            "NextAction": "e5ae829a-953e-4490-a846-c49518fa0bb3",
            "Condition": { "Operator": "TextStartsWith", "Operands": ["+8190"] }
          }
        ],
        "Errors": [
          {
            "NextAction": "52578cfa-32ab-4dc5-ae78-0829748b80e7",
            "ErrorType": "NoMatchingCondition"
          }
        ]
      }
    },
    {
      "Parameters": { "FlowLoggingBehavior": "Enabled" },
      "Identifier": "00e56ecd-f363-4a8b-b70b-e17616278f40",
      "Type": "UpdateFlowLoggingBehavior",
      "Transitions": { "NextAction": "564ac430-c916-4279-8907-60f96080b1a3" }
    },
    {
      "Parameters": {
        "TextToSpeechEngine": "Neural",
        "TextToSpeechStyle": "None",
        "TextToSpeechVoice": "Kazuha"
      },
      "Identifier": "564ac430-c916-4279-8907-60f96080b1a3",
      "Type": "UpdateContactTextToSpeechVoice",
      "Transitions": { "NextAction": "cbbace15-32aa-4949-b88b-90cc5bf21a1e" }
    },
    {
      "Parameters": { "LanguageCode": "ja-JP" },
      "Identifier": "cbbace15-32aa-4949-b88b-90cc5bf21a1e",
      "Type": "UpdateContactData",
      "Transitions": {
        "NextAction": "4ff267b8-6fa5-4256-ac19-6d0703fd81fc",
        "Errors": [
          {
            "NextAction": "4ff267b8-6fa5-4256-ac19-6d0703fd81fc",
            "ErrorType": "NoMatchingError"
          }
        ]
      }
    },
    {
      "Parameters": {
        "Attributes": { "input_phone_number": "$.StoredCustomerInput" },
        "TargetContact": "Current"
      },
      "Identifier": "90c242e6-360d-4be9-8cfa-645b33ac6a84",
      "Type": "UpdateContactAttributes",
      "Transitions": {
        "NextAction": "d5e69705-2a25-4beb-9e0e-a19c1218ae62",
        "Errors": [
          {
            "NextAction": "15562c61-adf4-4aeb-a6a6-b5a0282aae7f",
            "ErrorType": "NoMatchingError"
          }
        ]
      }
    },
    {
      "Parameters": {
        "Text": "ショートメッセージを送信しました。電話を切ります。"
      },
      "Identifier": "d1730443-5dea-4b16-a33a-fb8e25c4b607",
      "Type": "MessageParticipant",
      "Transitions": {
        "NextAction": "2906039d-70d3-416a-ba18-0b60b178e6ec",
        "Errors": [
          {
            "NextAction": "2906039d-70d3-416a-ba18-0b60b178e6ec",
            "ErrorType": "NoMatchingError"
          }
        ]
      }
    },
    {
      "Parameters": {
        "Text": "ご入力頂いた電話番号宛てに、ショートメッセージをお送りすることができません。電話を切ります。"
      },
      "Identifier": "9b196cfd-e7c7-4c60-9897-87390d7df8cc",
      "Type": "MessageParticipant",
      "Transitions": {
        "NextAction": "2906039d-70d3-416a-ba18-0b60b178e6ec",
        "Errors": [
          {
            "NextAction": "2906039d-70d3-416a-ba18-0b60b178e6ec",
            "ErrorType": "NoMatchingError"
          }
        ]
      }
    },
    {
      "Parameters": {},
      "Identifier": "2906039d-70d3-416a-ba18-0b60b178e6ec",
      "Type": "DisconnectParticipant",
      "Transitions": {}
    },
    {
      "Parameters": { "ComparisonValue": "$.Attributes.input_phone_number" },
      "Identifier": "d5e69705-2a25-4beb-9e0e-a19c1218ae62",
      "Type": "Compare",
      "Transitions": {
        "NextAction": "9b196cfd-e7c7-4c60-9897-87390d7df8cc",
        "Conditions": [
          {
            "NextAction": "2c2c44f1-1d41-487a-9e42-00700771103c",
            "Condition": { "Operator": "TextStartsWith", "Operands": ["070"] }
          },
          {
            "NextAction": "2c2c44f1-1d41-487a-9e42-00700771103c",
            "Condition": { "Operator": "TextStartsWith", "Operands": ["080"] }
          },
          {
            "NextAction": "2c2c44f1-1d41-487a-9e42-00700771103c",
            "Condition": { "Operator": "TextStartsWith", "Operands": ["090"] }
          }
        ],
        "Errors": [
          {
            "NextAction": "9b196cfd-e7c7-4c60-9897-87390d7df8cc",
            "ErrorType": "NoMatchingCondition"
          }
        ]
      }
    },
    {
      "Parameters": {
        "LambdaFunctionARN": "arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:function:cm-hirai-send_sms",
        "InvocationTimeLimitSeconds": "3",
        "ResponseValidation": { "ResponseType": "STRING_MAP" }
      },
      "Identifier": "e5ae829a-953e-4490-a846-c49518fa0bb3",
      "Type": "InvokeLambdaFunction",
      "Transitions": {
        "NextAction": "d1730443-5dea-4b16-a33a-fb8e25c4b607",
        "Errors": [
          {
            "NextAction": "15562c61-adf4-4aeb-a6a6-b5a0282aae7f",
            "ErrorType": "NoMatchingError"
          }
        ]
      }
    },
    {
      "Parameters": {
        "StoreInput": "True",
        "InputTimeLimitSeconds": "5",
        "Text": "お客様がただいまお使いの電話番号宛てに、ショートメッセージをお送りすることができません。メッセージの送信先となる別の電話番号をご入力下さい。",
        "DTMFConfiguration": { "DisableCancelKey": "False" },
        "InputValidation": { "CustomValidation": { "MaximumLength": "20" } }
      },
      "Identifier": "52578cfa-32ab-4dc5-ae78-0829748b80e7",
      "Type": "GetParticipantInput",
      "Transitions": {
        "NextAction": "90c242e6-360d-4be9-8cfa-645b33ac6a84",
        "Errors": [
          {
            "NextAction": "15562c61-adf4-4aeb-a6a6-b5a0282aae7f",
            "ErrorType": "NoMatchingError"
          }
        ]
      }
    },
    {
      "Parameters": { "Text": "エラーになりました。電話を切ります。" },
      "Identifier": "15562c61-adf4-4aeb-a6a6-b5a0282aae7f",
      "Type": "MessageParticipant",
      "Transitions": {
        "NextAction": "2906039d-70d3-416a-ba18-0b60b178e6ec",
        "Errors": [
          {
            "NextAction": "2906039d-70d3-416a-ba18-0b60b178e6ec",
            "ErrorType": "NoMatchingError"
          }
        ]
      }
    },
    {
      "Parameters": {
        "StoreInput": "False",
        "InputTimeLimitSeconds": "30",
        "SSML": "<speak>\n送信先電話番号は\n<say-as interpret-as=\"telephone\">$.Attributes.input_phone_number</say-as>\nでよろしいでしょうか。\n正しい場合は1を、修正する場合は2を押してください。\n</speak>"
      },
      "Identifier": "2c2c44f1-1d41-487a-9e42-00700771103c",
      "Type": "GetParticipantInput",
      "Transitions": {
        "NextAction": "15562c61-adf4-4aeb-a6a6-b5a0282aae7f",
        "Conditions": [
          {
            "NextAction": "e5ae829a-953e-4490-a846-c49518fa0bb3",
            "Condition": { "Operator": "Equals", "Operands": ["1"] }
          },
          {
            "NextAction": "52578cfa-32ab-4dc5-ae78-0829748b80e7",
            "Condition": { "Operator": "Equals", "Operands": ["2"] }
          }
        ],
        "Errors": [
          {
            "NextAction": "15562c61-adf4-4aeb-a6a6-b5a0282aae7f",
            "ErrorType": "InputTimeLimitExceeded"
          },
          {
            "NextAction": "15562c61-adf4-4aeb-a6a6-b5a0282aae7f",
            "ErrorType": "NoMatchingCondition"
          },
          {
            "NextAction": "15562c61-adf4-4aeb-a6a6-b5a0282aae7f",
            "ErrorType": "NoMatchingError"
          }
        ]
      }
    }
  ]
}

電話をかけると、以下の3つのパターンが確認できます。

  1. 発信元番号がSMS送信可能な「090」「080」「070」で始まる番号の場合、電話をかけると、すぐにSMSが送信され、通話が切断されます。
  2. 発信元番号がSMS送信可能な番号でない場合、プッシュ式でSMS送信可能な番号を入力すると、入力された番号が復唱されます。その後、「1」を入力するとSMSが送信され、「2」を押すと再度プッシュ式で電話番号を入力する必要があります。
  3. 発信元番号がSMS送信可能な番号でない場合、プッシュ式でSMS送信できない番号を入力すると、その番号にはSMSを送信できないとアナウンスし、通話を切断します。

[コンタクト属性を確認する]ブロック

発信元番号は「+81xxx」形式ですが、ユーザー入力の番号は「0xxx」形式です。

そのため、[コンタクト属性を確認する]ブロックが2つありますが、各々チェックするコンタクト属性の値が異なります。

[顧客の入力を取得する]ブロック

Amazon Connectの音声には、Amazon Pollyが利用されています。

Amazon Pollyは、深層学習を使用したテキスト読み上げサービスであり、SSML (Speech Synthesis Markup Language)タグを使用することで、音声の様々な側面をカスタマイズできます。

[顧客の入力を取得する]ブロックで電話番号を含めたアナウンスでは、以下の通り<say-as>タグを使用し、interpret-as="telephone"と指定することで、電話番号として解釈し、各桁を個別に読み上げます。

<speak>
送信先電話番号は
<say-as interpret-as="telephone">$.Attributes.input_phone_number</say-as>
でよろしいでしょうか。
正しい場合は1を、修正する場合は2を押してください。
</speak>

最後に

Amazon ConnectとLambdaを連携することで、SMS送信を柔軟にカスタマイズできることがわかりました。

発信元番号のチェックや、ユーザー入力の電話番号への送信など、ユースケースに合わせて色々な実装が可能です。

ポイントは以下の通りです。

  • 発信元番号とユーザー入力では、Lambdaに渡される電話番号の形式が異なる
  • Lambdaから返す値によって、Connectフローの遷移先を制御できる
  • SMS送信には文字数などの制限があるので注意が必要
  • Amazon PollyのSSMLタグを使うと、電話番号を適切に読み上げられる

本記事が参考になれば幸いです。

参考

https://docs.aws.amazon.com/ja_jp/polly/latest/dg/supportedtags.html

https://docs.aws.amazon.com/ja_jp/sns/latest/dg/sms_publish-to-phone.html