S3に配置したForm画面より情報を入力しSlackへ投稿してみる

記事タイトルとURLをコピーする

こんにちは。技術5課の芳賀です。最近、CloudFront+S3の組み合わせ、API Gateway+Lambdaの組み合わせでサービスを触っていました。ふと、これを全部組み合わせて何か作れないか・・・と思い今回のネタを思いつきました。

思いついた内容としては以下になります。

  1. CloudFront経由でS3のコンテンツ(フォーム)を参照
  2. S3に配置したフォームより情報を入力
  3. S3のフォームよりAPI Gatewayを実行
  4. APIGatewayをトリガーにLambdaを実行
  5. LambdaよりSlackへ投稿

簡単な図にしてみると以下のようなイメージです。

そして今回S3に配置するフォームは以下のようなイメージになります(とてもシンプル!)

図の後ろの方から準備していきたいと思います。
既出のネタもありますので、要所要所で割愛させて頂きますがご了承ください。

1.Slackの設定


Slackアプリではなく、カスタムインテグレーションの方の「Incoming Webhook」を設定します。
設定方法についてはいろいろな方が書かれているので割愛させて頂きます。

※カスタムインテグレーションの「Incoming Webhook」は現在非推奨となっています。そのためSlackへ投稿する部分についてはSlackのAPIを使用することをお勧めします(恥ずかしながらこのブログを書く際に非推奨と分かりました。)

以下のようにSlackの画面を確認すると最新のAPIを使用することをお勧めされます。

2.Lambda関数の作成

LambdaからSlackへ通知するためのLambda関数を作成します。以下の内容を入力し、「関数の作成」を選択します。

  • 関数名:任意。今回は「SendTOSlack」としました(TOの「O」を小文字にしたつもりでしたが大文字になってました)。
  • ランタイム:「Pyathon3.8」を選択。
  • 実行ロール:「基本的なLambdaアクセス権限で新しいロールを作成」を選択。

画面が遷移し、関数コードの項目を確認するとデフォルトで「lambda_function.py」が作成されています。以下のコードをコピペして上書きします。

ざっくりとプログラムの中身を説明すると、「lambda_handler」関数の「event」引数にS3に配置したフォームで入力した情報が入っています。
フォームで入力した情報というのはSlackへの投稿者、アイコン、投稿先チャンネル、投稿内容です。この内容を取得し、Slackの「Incoming Webhook」に対して情報を渡すことでSlackへ投稿するようなコードになっています。

import boto3
import json
import urllib
import os

def lambda_handler(event, context):

    SLACK_POST_URL = os.environ['SLACK_POST_URL'] # SlackのIncoming WebhooksのURL
    username =  urllib.parse.unquote(event['name'])
    icom =  urllib.parse.unquote(event['icon'])
    message = urllib.parse.unquote(event['message'])
    channnel = urllib.parse.unquote(event['channel'])

    # メッセージの内容
    send_data = {
        "username": username,
        "icon_emoji": icom,
        "text": message,
        "channel": channnel,
        "link_names": 1,
    }

    send_text = ("payload=" + json.dumps(send_data)).encode('utf-8')

    result = urllib.request.Request(
        SLACK_POST_URL,
        data=send_text,
        method="POST"
    )

    with urllib.request.urlopen(result) as response:
        response_body = response.read().decode('utf-8')

    return response_body

ちなみにos.environ['SLACK_POST_URL']の部分は、Lambdaの環境変数を参照する記述になっています(プログラムにベタ書きは格好が悪かったので)。
以下のようにSlackのIncoming WebhookのURLを設定しておきます。

3.API Gatewayの作成

関数を作成したらAPI Gatewayのトリガーを設定していきます。先ほど作成したLambda関数のDesignerより、「トリガーを追加」を選択します。

以下の内容を入力し「追加」を選択します。

  • トリガー:API Gateway
  • API:Create an API
  • API type:REST API
  • セキュリティ:オープン(今回は検証のためオープンを選択しています)

「SendTOSlack-API」というAPIが作成されました。

「SendTOSlack-API」の設定をします。リソースの「SendTOSlack」を選択した状態で「アクション」→「メソッドの作成」を選択します。

「POST」を選択し、右にあるチェックマークを選択します。

「POST」メソッドが作成されます。SendTOSlackの「POST」を選択、以下の内容を入力し「保存」を選択します。

  • 統合タイプ:Lambda関数
  • Lambdaリージョン:ap-northeast-1
  • Lambda関数:SendTOSlack
  • デフォルトタイムアウトの使用:任意(ここではチェックを入れました)

確認ダイアログが表示されるので「OK」を選択します。

以下のような画面になるので(ならなかったらもう一度「POST」を選択)、「統合リクエスト」のリンクを選択します。

遷移した先の画面下に「マッピングテンプレート」という項目があるので表示させます。

「マッピングテンプレートの追加」を選択、以下を入力して右のチェックマークを選択します。

  • Content-Type:application/x-www-form-urlencoded

Content-Typeはどのような形式でリクエストパラメータを受け取るかの設定で、他に「application/json」や「application/xml」などがあります。

確認ダイアログが表示されるので「はい、この統合を保護します」を選択します。

マッピングテンプレートの入力項目が表示されるので、以下の内容を入力して「保存」を選択します。

{
 #set( $param = $input.body )
 #foreach( $keyValue in $param.split( '&' ) )
 #set( $keyValueArray = $keyValue.split( '=' ) )
        "$keyValueArray[0]" : "$keyValueArray[1]"#if( $foreach.hasNext ),#end
 #end
}

このマッピングテンプレートというものがなかなかの曲者で、理解するのにかなりの時間を要しました(未だに完全には理解していませんが・・・)。
上記の設定で何をしているかというと、今回作成しようとしているS3のフォームから入力された内容をJSON形式に変換するような処理になっています。

例えばS3のフォームで以下のような内容でSendTOSlack-APIにPOSTしたとします。

  • 投稿者:haga
  • アイコン::eyes:
  • 投稿先チャンネル:#general
  • 投稿内容:テストテスト

すると以下のような内容でAPIは情報を受けとります。

name=haga&icon=:eyes:&channel=#general&message=テストテスト

上記の内容を「&」、「=」といった記号から分解し、以下のようなJSON形式に変換した形でLambdaへ渡します。

{
 name:haga
 icon::eyes:
 channel:#general
 message:テストテスト
}

フォームからPOSTする際にJSON形式に変換してPOST、Lambda関数側でJSON形式に変換するなどの方法も可能ですが、今回はこのマッピングテンプレートを使用してみました。

マッピングテンプレートについては以下のドキュメントが参考になるかと思います。
API Gateway マッピングテンプレートとアクセスのログ記録の変数リファレンス


「SendTOSlack-API」の設定が完了したので、APIが使用できるようデプロイします。
リソースより「POST」を選択、「APIのデプロイ」を選択します。

デプロイされるステージ、デプロイメントの説明を入力し「デプロイ」を選択します。
今回ステージは「default」を選択しました。

以下のような画面に遷移します。
「URLの呼び出し」にあるURLを入力することでAPIが実行されます。後ほどS3に配置するフォームに埋め込むのでURLをメモしておきます。

4.S3バケットの作成とCloudFrontの設定

この設定は以下のブログを参考にCloudFrontを設定、S3のコンテンツを配置します(ほぼやりたいこと通りなのでここでの詳細は省きます)。
http://blog.serverworks.co.jp/tech/2020/05/10/post-84290/

S3バケットを作成します。
ここでは「sendtoslack-testbucket」という名前でデフォルト設定のS3バケットを作成しました。

配置するコンテンツは以下です。
index.htmlはバケットの直下に配置します。「slack.css」は「css」フォルダを作成してその配下に配置します。
(HTML、CSSは得意ではないので温かい目でコードを見て頂ければと思います・・・)

【index.html】

<!DOCTYPE html>
<html>
  <head>
      <meta charset="utf-8">
      <link rel="stylesheet" type="text/css" href="css/slack.css">
      <script type="text/javascript">
        var $form = $('#sendMessageForm')
        var $trigger = $('#sendMessageTrigger')

        $trigger.click(function() {

            $form.submit();
            $form[0].reset();

            return false;
        });
      </script>
  </head>
 
  <body>
  <p class="lead-form">Slackへ投稿!!</p>

    <form name="myform" id="sendMessageForm" action="<API Gatewayで作成したURLを指定する>" method="post" accept-charset="utf-8" target="sendMessage">
      
      <div class="item">
        <label class="label">投稿者</label>
        <input class="inputs" type="text" name="name">
      </div>
      
      <div class="item">
        <label class="label">アイコン</label>
        <input class="inputs" type="text" name="icon">
        <label class="notice">:〇〇:の形式でアイコンを指定</label>
      </div>

      <div class="item">
        <label class="label">投稿先チャンネル</label>
        <input class="inputs" type="text" name="channel">
        <label class="notice">#〇〇で指定したチャンネルへ投稿</label>
      </div>

      <div class="item">
        <label class="label">投稿内容</label>
        <textarea class="inputs" type="text" name="message" placeholder="投稿内容を入力"></textarea>
      </div>

      <div class="btn-area">
        <input id="sendMessageTrigger" type="submit" value="投稿">
      </div>

    </form>
    <iframe name="sendMessage" style="width:0px;height:0px;border:0px;"></iframe>
  </body>
</html>

上記コードの<form>タグ内のactionには、「3.API Gatewayの作成」で取得したURLを"<API Gatewayで作成したURLを指定する>"の箇所に記述してください。

【slack.css】

.lead-form{
  text-align: center;
  font-size:28px;
}
form{
  width:460px;
  margin:0 auto;
}
.item{
  overflow: hidden;
  margin-bottom: 20px;
}
.label{
  float: left;
  margin-right: 20px;
  width:430px;
  padding-left: 10px;
}
.inputs{
  float: left;
  width:430px;
}
.notice{
  float: left;
  margin-right: 20px;
  width:430px;
  padding-left: 10px;
  font-size:12px;
  text-align: right;
}
input[type="text"] {
  border: solid 1px;
  border-radius:5px;
  padding:10px;
  font-size: 15px;
}
textarea{
  border: solid 1px;
  border-radius:5px;
  padding: 10px;
  height: 160px;
  font-size: 15px;
}

input[type="submit"]{
  border: none;
  font-size:17px;
  font-weight:bold;
  padding: 10px 20px;
  margin: 0 5px;
}
.btn-area{
  text-align: right;
}

CloudFrontを設定していきます。
設定は先ほど紹介したブログと同様の設定でOKです。1点、ディストリビューション作成時に設定する「Default Root Object」が「index.html」になる点だけが異なります。

ステータスが「Deployed」になるまで暫く待ち、デプロイが完了したら作成したディストリビューションのドメイン名を指定してブラウザでアクセスしてみます(デプロイ直後はアクセスできず、1時間ほど待ちました。コンテンツの反映に時間がかかる場合がありそうです)。

アクセスすると、冒頭で紹介したシンプルなWeb画面が表示されます。
以下の内容でSlackへ投稿してみました。

無事、Slackへ投稿できました!!

最後に

どうしても一度、入力フォームを持ったサーバレスなアプリケーションを作成したく今回のような内容となりました。API GatewayとLambdaの部分はServerless Frameworkを使うともっと効率よくデプロイできそうな気がしますが、情報が盛り沢山になりそうだったのであえてマネージメントコンソール上からの構築としました。

入力フォームを持ったサーバレスアプリケーションを構築したい方の参考になれば幸いです。

芳賀 祐介(記事一覧)