サーバーレス 問い合わせフォーム AWS s3/lambda/cognito/ses/slack

webサーバを使用せず、s3 でwebを公開してる際のお問い合わせフォームの実装方法です。

前提

  • Aws s3にてwebサイト公開済み
  • slackアカウントを持っている
  • sesにて特定のdomainにて認証済み

※参考
yarn と parcel と riot を使って簡単な webサイトを作ってみた
Webpack + Riot + Materializeで、Webサイト環境を作った話

処理の流れ

  • 問い合わせフォームからs3にjsonファイルがupされる(congnitoを使って)
  • s3にアップされると、lambda関数が処理実行
  • lambda関数がses(ユーザー用)とslack(管理用)に送信

やること

  • s3にbucket作成
  • cognito設定
  • slackの準備
  • sesの準備
  • lambda設定
  • フォーム実装

s3にbucket作成

s3にフォームからupされる専用バケットを作成します。
一応指定のサイト以外からアップされないように「CORS設定」をしておく(本番反映後設定したほうがローカルでの開発中に楽)

アクセス権限 -> CORSの設定

<!-- Sample policy -->
<CORSConfiguration>
  <CORSRule>
    <AllowedOrigin>開発中は * 公開後 公開ドメインに変えた方がいい</AllowedOrigin>
    <AllowedMethod>PUT</AllowedMethod>
    <MaxAgeSeconds>3000</MaxAgeSeconds>
    <AllowedHeader>*</AllowedHeader>
  </CORSRule>
</CORSConfiguration>

cognito設定

Cognitoについては下記を参考にすると理解できそうです。
https://dev.classmethod.jp/cloud/aws/what-is-the-cognito/

cognito IDプールを作成する

  • AWSコンソールより、Cognitoを選択し、「フェデレーテッドアイデンティティの管理」を洗濯、右上の「新しいIDプールの作成」を選択。
  • 「ID プール名」を、わかりやすい名前に
  • 「認証されていない ID に対してアクセスを有効にする」にチェックを入れる
  • 「プールを作成」する

次の画面に進むみ、詳細を表示
キャプチャ
jsコードを控えておく

ポリシーを作成し、ロールにアタッチする

作成されたcognitoのロールに対して、s3バケットへのファイルup権限を追加します。

  • AWSコンソールより、IAMを選択し、ポリシーを選択。
  • ポリシーの作成をクリックし、JSONに切り替える
  • 下記をコピペ(指定したs3のarnに対してput権限を付与する)

    {

    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:PutObjectAcl"
            ],
            "Resource": [
                "arn:aws:s3:::先程作成したバケット名/*",
                "arn:aws:s3:::先程作成したバケット名/*"
            ]
        }
    ]
    

    }

  • 作成をクリックし、次の画面で、ポリシー名にわかりやすい名前を入れて作成する

作成されたポリシーをロールにアタッチする

※ IDプール作成時にポリシー詳細にて設定したほうが楽かも

  • AWSコンソールより、IAMを選択し、ロールを選択。
  • 一覧より、作成された、「Cognito_xxxxxxxxxxxxxUnauth_Role」を選択。※Unauthの方
  • ポリシーのアタッチ
  • 上記作成したポリシーを検索欄より、検索し、チェックを入れる
  • ポリシーのアタッチ

※ この手順は、cognito IDプール作成じにポリシーの設定からやったほうが楽かもしれません。

一応、これでcognitoの準備はOKです。

slackの準備

とりあえずこちらの方法を参考にさせていただきました。
http://reiki4040.hatenablog.com/entry/2017/01/30/001634
webhookのURLだけ、コピーして控えておきます。

sesの準備

sesは、リージョンが限られているので、限られたリージョンの中から、選択する。

  • Aws コンソールより、sesを選択
  • 左メニューの「Email address」を選択し、通知元のEmailアドレスを指定する
  • Verify This Email Adressを選択する
  • 登録したアドレスに承認メールが届くので承認する

lambda設定

指定のS3バケットにデータがupされたら、upされたファイルの情報を元に、そのメールアドレスへ送付するし、管理側には、slackにて、通知する
最初に作成した、s3のバケットを使用します。

まずは、lambdaより、sesへのアクセスを行うのに、ロールの設定をします。

ロールを作成

  • IAMから、ポリシー、ポリシーの作成
  • Awsサービスをlambdaを選択し、次のステップ
  • ポリシータイプで、sesと検索し、「AmazonSESFullAccess」にチェックを入れて次のステップ
  • また、cloudwatchへの権限も追加するので「AWSOpsWorksCloudWatchLogs」と検索し、チェック
  • ロール名にわかりやすい名前を入れる

lambda関数を作成

Awsコンソールより、Lambdaを選択し、関数の作成をクリック

  • 名前 -> わかりやすい名前
  • ランタイム -> node6.10
  • ロール -> 既存のロールを選択
  • 既存のロール -> 先程作成したsesとcloudwatchのポリシーを持つロールを指定

これで関数の作成

  • 環境変数に「SLACK_WEBHOOK_URL」のkey名で、先程のslackのwebhookのURLを指定する
'use strict'
const aws = require('aws-sdk');
const s3 = new aws.S3({ apiVersion: '2006-03-01' });

console.log('Loading function');

// Set for slack.
const https = require('https');
const url = require('url');
const slack_url = process.env.SLACK_WEBHOOK_URL;
const slack_req_opts = url.parse(slack_url);
slack_req_opts.method = 'POST';
slack_req_opts.headers = {'Content-Type': 'application/json'};

exports.handler = (event, context, callback) => {
    //console.log('Received event:', JSON.stringify(event, null, 2));

    // Get the object from the event and show its content type
    const bucket = event.Records[0].s3.bucket.name;
    const key = decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, ' '));
    const params = {
        Bucket: bucket,
        Key: key,
    };
    s3.getObject(params, (err, data) => {
        if (err) {
            console.log(err);
            const message = `Error getting object ${key} from bucket ${bucket}. Make sure they exist and your bucket is in the same region as this function.`;
            console.log(message);
            callback(message);
        } else {
            const message = JSON.parse(data.Body);
            const body = [
                '[お名前] ' + message.name,
                '[メールアドレス] ' + message.email,
                '[日時] ' + message.date,
                '[お問い合わせ内容]' + "\n" + message.body,
            ].join("\n");

            // slack
            const req = https.request(slack_req_opts, function (res) {
                if (res.statusCode === 200) {
                    context.succeed('posted to slack');
                } else {
                    context.fail('status code: ' + res.statusCode);
                }
            });

            req.on('error', function(e) {
                console.log('problem with request: ' + e.message);
                context.fail(e.message);
            });

            req.write(JSON.stringify({text: "お問い合わせがありました\n\n" + body}));
            req.end();

            // ses
            const email = {
                Source: "xxxxxxxxxxxxxxxxxx",
                Destination: { ToAddresses: [message.email] },
                Message: {
                    Body: { Text: { Data: message.name + "様\n\nテスト送信\n\n" + body} },
                    Subject: { Data: "お問い合わせありがとうございます" },
                },
            };
            const ses = new aws.SES({ region: 'us-east-1' });
            ses.sendEmail(email, callback);
        }
    });
};

トリガーの設定

左側のリストからトリガーを追加します。

  • s3を選択
  • ページ下記にて、バケット名の指定
  • ObjectCreatedByPut を指定する

フォーム実装

例では、roit.jsを使っていますが、必要に応じて変更して下さい。

$ yarn add aws-sdk

コード

// Amazon Cognito 認証情報プロバイダーを初期化します
AWS.config.region = 'us-west-2';
AWS.config.credentials = new AWS.CognitoIdentityCredentials({
    IdentityPoolId: 'xx-xxxx-xx:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
});
submit (event) {
  event.preventDefault()

  let s3BucketName = "バケット名"

  let email = this.refs.email.value
  let name = this.refs.name.value
  let body = this.refs.message.value
  let submit = this.refs.submit
  submit.disabled = true

  let now = new Date();
  let obj = {"name":name, "email":email , "body":body, "date": now.toLocaleString()}
  let s3 = new AWS.S3({params: {Bucket: s3BucketName}});
  let blob = new Blob([JSON.stringify(obj, null, 2)], {type:'text/plain'});
  s3.putObject({Key: "uploads/" +now.getTime()+".txt", ContentType: "text/plain", Body: blob, ACL: "public-read"},
    function(err, data){
      if(data !== null){
          成功処理
      }
      else{
          失敗処理
      }
    }
  );

webに公開後

s3のバケットのCORSの設定で、ドメインを指定して、指定ドメイン以外からfileをアップできないようにしておきましょう。

まとめ

cognito のIDプールを使用して、s3へファイルアップ、lambda関数より検知して、sesとslackにメッセージを送信することができました。
ec2等webサーバーを使っていればサーバ側で処理をして実装可能ですが、サーバーレスにして、この方法でも簡単に実装でき、トータル的な料金も、全然安いです。
ちょっとしたwebサイトなら本当にwebサーバーは不要になってきましたね。

ご意見ご指摘があったらお願いします。