Amplify で お問い合わせフォーム を作ってみた

こんにちは。奥です。 本日はAmplifyでちょこっとアプリを作ってみました。
タイトル通り、お問い合わせフォームをちょこっと作ってみました。

Amplifyとは

私の中でのAmplifyの解釈はこんな感じです

  • AWSでのモバイル/ウェブアプリの開発を簡単にするためのもの
  • AWS側である程度バックエンドをよしなにやってくれるので設計が楽になる

それで公式には以下のように書かれています。

AWS Amplify は、AWS を使用したスケーラブルなモバイルアプリおよびウェブアプリの作成、設定、実装を容易にします。Amplify はモバイルバックエンドをシームレスにプロビジョニングして管理し、バックエンドを iOS、Android、ウェブ、React Native のフロントエンドと簡単に統合するためのシンプルなフレームワークを提供します。また、Amplify は、フロントエンドとバックエンドの両方のアプリケーションリリースプロセスを自動化し、機能をより迅速に提供することができます。

モバイルアプリケーションでは、オフラインのデータ同期、ストレージ、複数のユーザー間でのデータ共有など、デバイス上で直接実行できないアクションに対してクラウドサービスが必要です。バックエンドを強化するために、多くの場合、複数のサービスを設定、セットアップ、管理する必要があります。また、複数のコード行を記述することによって、これらの各サービスをアプリケーションに統合する必要があります。しかし、アプリケーションの機能が増えるにつれて、コードとリリースプロセスが複雑になり、バックエンドの管理に多くの時間が必要になります。

Amplify はモバイルアプリケーションのバックエンドをプロビジョニングし、管理します。認証、分析、またはオフラインのデータ同期など、必要な機能を選択するだけで、Amplify はそれぞれの機能を強化する AWS サービスを自動的にプロビジョニングして管理します。Amplify ライブラリと UI コンポーネントを使用して、これらの機能をアプリケーションに統合することができます。

AWS Amplify

Amplifyについての記載は弊社諏訪の記事がより詳しいのでそちらをご参照ください。

AWSの次世代JavaScriptライブラリ「AWS Amplify」の概要とReactアプリに導入する手順 #serverless #adventcalendar

今回作るもの

バックエンドにAPI GWとLambda、SESを使用してフロントエンドにS3を使用したアーキテクチャでお問い合わせフォームを作ろうと思います。
とりあえずフロントエンドからAPIを通じてお問い合わせ内容をメールで送信できるところがゴールです。

こちらのリポジトリにソースを公開しているのでご参照ください。

1. 前準備

基本的にGetting Startedに従って初期設定しています。

amplify init でプロジェクトの初期設定を行います。
Assume Roleを使用した環境でも問題なく設定できたのが結構衝撃的でした。
MFAを有効にしている場合は、It requires MFA authentication.の表示が出た段階で、MFA Codeを打ち込みましょう。
うまくMFAの表示がされてない場合がありますので、めげずに打ち込みましょう。

$ npm install -g @aws-amplify/cli
$ npm install --save aws-amplify
$ amplify init

  ? Enter a name for the project amplify-form
  ? Enter a name for the environment prod
  ? Choose your default editor: Vim (via Terminal, Mac OS only)
  ? Choose the type of app that you're building javascript
  Please tell us about your project
  ? What javascript framework are you using none
  ? Source Directory Path:  src
  ? Distribution Directory Path: dist
  ? Build Command: npm run build
  ? Start Command: npm run start
  Using default provider  awscloudformation
  
  For more information on AWS Profiles, see:
  https://docs.aws.amazon.com/cli/latest/userguide/cli-multiple-profiles.html

  ? Do you want to use an AWS profile? Yes
  ? Please choose the profile you want to use hoge
  ⠋ Initializing project in the cloud...Profile hoge is configured to assume role
    arn:aws:iam::111111111111:role/hoge
  It requires MFA authentication. The MFA device is
    arn:aws:iam::111111111111:mfa/hoge
    ? Enter the MFA token code: 111111
  ⠦ Initializing project in the cloud...
   ~~~
   ~~~

2. Webpackの設定

Amplify関連のライブラリを使用するので、Webpackを使用してコンパイルできるように準備します。

$ npm install --save-dev webpack webpack-cli webpack-dev-server copy-webpack-plugin

そのあとに、webpack.config.js を作成して、Wepackでのビルドができるようにします。

const CopyWebpackPlugin = require('copy-webpack-plugin');
const webpack = require('webpack');
const path = require('path');

module.exports = {
  mode: 'development',
  entry: './src/app.js',
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/
      }
    ]
  },
  devServer: {
    contentBase: './dist',
    overlay: true,
    hot: true
  },
  plugins: [
    new CopyWebpackPlugin(['index.html']),
    new webpack.HotModuleReplacementPlugin()
  ]
}

ついでにpacka.jsonも編集します。scirptの部分にビルドとローカル開発用のスクリプトを入れます。

{
  "scripts": {
    "build": "webpack",
    "start": "webpack && webpack-dev-server --mode development"
  },
  "dependencies": {
    "aws-amplify": "^1.1.29"
  },
  "devDependencies": {
    "copy-webpack-plugin": "^5.0.3",
    "webpack": "^4.35.0",
    "webpack-cli": "^3.3.4",
    "webpack-dev-server": "^3.7.2"
  }
}

3. バックエンドの追加

まずは、デプロイするための準備をします。

$ amplify add api

  ? Please select from one of the below mentioned services REST
  ? Provide a friendly name for your resource to be used as a label for this category in the project: amplifyFormAPI
  ? Provide a path (e.g., /items) /inquiry
  ? Choose a Lambda source Create a new Lambda function
  ? Provide a friendly name for your resource to be used as a label for this category in the project: amplifyFormAPI
  ? Provide the AWS Lambda function name: amplifyFormAPI
  ? Choose the function template that you want to use: Serverless express function (Integration with Amazon API Gateway)
  ? Do you want to access other resources created in this project from your Lambda function? Yes
  ? Select the category (Press <space> to select, <a> to toggle all, <i> to invert selection)
  
  You can access the following resource attributes as environment variables from your Lambda function
  var environment = process.env.ENV
  var region = process.env.REGION
  
  ? Do you want to edit the local lambda function now? No
  Succesfully added the Lambda function locally
  ? Restrict API access No
  ? Do you want to add another path? No
  Successfully added resource amplifyFormAPI locally
  
  Some next steps:
  "amplify push" will build all your local backend resources and provision it in the cloud
  "amplify publish" will build all your local backend and frontend resources (if you have hosting category added) and provision it in the cloud

$ amplify push

この段階で、amplify/backend/api/amplify/backend/function/ が作成されているのが確認できます。
functionの中にはLambda関数を書いて、アップロードすることで、APIへの変更を加えられます。

なので、API、フロントのコードを書いていきましょう。

4. バックエンドの準備

クライアントから、名前Eメールアドレスお問い合わせ内容が送られてくる想定で作成します。
そして受け取った内容をSESを通じて管理者に送るようにします。

a. 必要ライブラリのインストール

$ cd amplify/backend/function/amplifyFormAPI/src
$ npm install aws-sdk

b. Lambda関数の変更

リクエストを受け取って指定したメールアドレスにメールを送信できるようにします。
GitHubでのコードはこちらです。

/*
Copyright 2017 - 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at
    http://aws.amazon.com/apache2.0/
or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and limitations under the License.
*/

/* Amplify Params - DO NOT EDIT
You can access the following resource attributes as environment variables from your Lambda function
var environment = process.env.ENV
var region = process.env.REGION

Amplify Params - DO NOT EDIT */

const AWS = require("aws-sdk")
const express = require("express")
const bodyParser = require("body-parser")
const awsServerlessExpressMiddleware = require("aws-serverless-express/middleware")

const app = express()
app.use(bodyParser.json())
app.use(awsServerlessExpressMiddleware.eventContext())

// Enable CORS for all methods
app.use( (req, res, next) => {
  res.header("Access-Control-Allow-Origin", "*")
  res.header(
    "Access-Control-Allow-Headers",
    "Origin, X-Requested-With, Content-Type, Accept"
  )
  next()
})

app.post("/inquiry", async (req, res) => {
  if (! req.body.name || ! req.body.email || ! req.body.inquiry) {
    console.log("req.body.* not found...")
    res.status(400).send("More Pamaraters are Required")
    return
  }

  const params = {
    Destination: {
      ToAddresses: [process.env.ADMIN_EMAIL]
    },
    Message: {
      Body: {
        Html: {
          Charset: 'UTF-8',
          Data: `<html lang="ja"><head><meta charset="utf-8"></head><body><h3>名前</h3><br/><p>${req.body.name}</p><br/><h3>メールアドレス</h3><br><p>${req.body.email}</p><h3>お問い合わせ内容</h3><br><p>${req.body.inquiry}</p></body></html>`
        },
        Text: {
          Charset: 'UTF-8',
          Data: `名前: ${req.body.name} \nメールアドレス: {req.body.email} \nお問い合わせ内容: ${req.body.inquiry}`,
        },
      },
      Subject: {
        Charset: 'UTF-8',
        Data: 'お問い合わせを受け付けました'
      },
    },
    Source: process.env.ADMIN_EMAIL,
  }
  console.log("set params to send an email")

  AWS.config.update({ region: "us-east-1" })
  const ses = new AWS.SES()
  try {
    await ses.sendEmail(params).promise()
    console.log("Success to Send an Email")
    res.json({ success: "post call succeed!" })
    return
  } catch (e) {
    console.log(`Failed to Send an Email: ${e}`)
    res.status(500).send("Internal Server Error" )
    return
  }
})

app.listen(3000, () => {
  console.log("App started")
})

module.exports = app

ここまでできたら、いったん変更を全て反映させましょう。

$ amplify update function
Using service: Lambda, provided by: awscloudformation
? Please select the Lambda Function you would want to update amplifyFormAPI
? Do you want to update permissions granted to this Lambda function to perform on other resources in your project? Yes
? Select the category (Press <space> to select, <a> to toggle all, <i> to invert selection)

You can access the following resource attributes as environment variables from your Lambda function
var environment = process.env.ENV
var region = process.env.REGION

? Do you want to edit the local lambda function now? No
Successfully updated resource

$ amplify push

Current Environment: prod

| Category | Resource name  | Operation | Provider plugin   |
| -------- | -------------- | --------- | ----------------- |
| Function | amplifyFormAPI | Update    | awscloudformation |
| Api      | amplifyFormAPI | No Change | awscloudformation |
? Are you sure you want to continue? Yes
Profile hoge is configured to assume role
  arn:aws:iam::111111111111:role/hoge
It requires MFA authentication. The MFA device is
  arn:aws:iam::111111111111:mfa/hoge
? Enter the MFA token code: 111111

c. Lambda用のIAM Roleのアップデート

json形式で書かれたテンプレートファイルがあるので変更を加えます。
Lambdaへの環境変数の渡し方は実際はAWS Secrets Manager がベストだと思います。
Updateの方法次第では、CloudFormation のテンプレートから環境変数が削除されたりすることがあるので別途準備して読み込ませるとコード上での変更のみで対応可能なので楽になります。今回はサンプルなのでこのような形にしています。

  • 環境変数 ADMIN_EMAIL の追加("ADMIN_EMAIL": "hoge@example.com" を任意の値に変更します。これあてにメールが送信されます。)
  • SESを使用できるようにポリシーの追加
{
  "Resources": {
    "LambdaFunction": {
      "Type": "AWS::Lambda::Function",
      "Metadata": {
        "aws:asset:path": "./src",
        "aws:asset:property": "Code"
      },
      "Properties": {
        "Handler": "index.handler",
        "FunctionName": {
          "Fn::If": [
            "ShouldNotCreateEnvResources",
            "amplifyFormAPI",
            {
              "Fn::Join": [
                "",
                [
                  "amplifyFormAPI",
                  "-",
                  {
                    "Ref": "env"
                  }
                ]
              ]
            }
          ]
        },
        "Environment": {
          "Variables": {
            "ENV": {
              "Ref": "env"
            },
            "REGION": {
              "Ref": "AWS::Region"
            },
            "ADMIN_EMAIL": "hoge@example.com"
          }
        },
        "Role": {
          "Fn::GetAtt": [
            "LambdaExecutionRole",
            "Arn"
          ]
        },
        "Runtime": "nodejs8.10",
        "Timeout": "25",
        "Code": {
          "S3Bucket": "amplify-form-prod-xxxxxxxxxxx-deployment",
          "S3Key": "amplify-builds/amplifyFormAPI-xxxxxxxxxxx-latest-build.zip"
        }
      }
    },
}
{
  "Resources": {
    ~~~
    "lambdaexecutionpolicy": {
      "DependsOn": [
        "LambdaExecutionRole"
      ],
      "Type": "AWS::IAM::Policy",
      "Properties": {
        "PolicyName": "lambda-execution-policy",
        "Roles": [
          {
            "Ref": "LambdaExecutionRole"
          }
        ],
        "PolicyDocument": {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Effect": "Allow",
              "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents",
                "ses:*",
              ],
              "Resource": {
                "Fn::Sub": [
                  "arn:aws:logs:${region}:${account}:log-group:/aws/lambda/${lambda}:log-stream:*",
                  {
                    "region": {
                      "Ref": "AWS::Region"
                    },
                    "account": {
                      "Ref": "AWS::AccountId"
                    },
                    "lambda": {
                      "Ref": "LambdaFunction"
                    }
                  }
                ]
              }
            }
          ]
        }
      }
~~~
}

変更を反映させます。

$ amplify update function
Using service: Lambda, provided by: awscloudformation
? Please select the Lambda Function you would want to update amplifyFormAPI
? Do you want to update permissions granted to this Lambda function to perform on other resources in your project? No
? Select the category (Press <space> to select, <a> to toggle all, <i> to invert selection)

You can access the following resource attributes as environment variables from your Lambda function
var environment = process.env.ENV
var region = process.env.REGION

? Do you want to edit the local lambda function now? No
Successfully updated resource

$ amplify push

Current Environment: prod

| Category | Resource name  | Operation | Provider plugin   |
| -------- | -------------- | --------- | ----------------- |
| Function | amplifyFormAPI | Update    | awscloudformation |
| Api      | amplifyFormAPI | No Change | awscloudformation |
? Are you sure you want to continue? Yes
Profile hoge is configured to assume role
  arn:aws:iam::111111111111:role/hoge
It requires MFA authentication. The MFA device is
  arn:aws:iam::111111111111:mfa/hoge
? Enter the MFA token code: 111111

d. SESのセットアップ

下記記事を参考に設定してください
今回は検証が目的なので、検証用のEメールを追加してそれで終わらせています。

Amazon SESによるメール送信環境の構築と実践

5. フロントエンドの準備

バックエンドの準備ができたので次はフロントエンドです。
フォームをHTMLで作って、APIコールをJavaScriptから行えば完成です。  

a. コーディング

APIに対して問い合わせフォームの内容を送信できるようにサクッと実装します。
実装するとこんな感じのフォームが出来上がります。ややcssの手抜き感が否めないですが、お許しください。

<!DOCTYPE html>
<html lang="jp">
<head>
  <meta charset="utf-8"/>
  <style>
    table th,
    table td {
      border: solid 1px black;
    }

    table {
      border-collapse: collapse;
    }
  </style>
</head>
<body>
  <form id="submit_form">
    <table>
      <tr>
        <td> お名前 </td>
        <td><input type="text" name="name" style="width: 70%;" /></td>
      </tr>
      <tr>
        <td> E mail </td>
        <td><input type="text" name="email" style="width: 70%;" /></td>
      </tr>
      <tr>
        <td> お問い合わせ内容 </td>
        <td> <textarea name="inquiry" rows="5" cols="100"></textarea> </td>
      </tr>
    </table>
    <button id="submit_button">送信する</button>
  </form>
  <script src="main.bundle.js"></script>
</body>
</html>

import Amplify, { API } from 'aws-amplify'
import awsconfig from './aws-exports'

Amplify.configure(awsconfig)

const postInquiry = async body => {
  const APIName = 'amplifyFormAPI'
  const path = '/inquiry'
  const params = {
    body: body,
  }
    return await API.post(APIName, path, params)
}

submit_button.addEventListener('click', async event => {
  event.preventDefault()
  const form = document.getElementById("submit_form")

  if (form.inquiry.value === '' || form.name.value === '' || form.email.value === '') {
    window.alert('全項目を入力してください')
    return
  }

  try {
    await postInquiry({inquiry: form.inquiry.value, name: form.name.value, email: form.email.value})
    window.alert('お問い合わせの送信が完了しました。')
  } catch (e) {
    window.alert('お問い合わせの送信に失敗しました。')
  }

})

b. ホスティングの開始

s3バケットにコードをビルドしてから送ります。hosting bucket name で任意の名前を指定してください。
Select the environment setup でProdを選択すると、CloudFrontを使ったり本番環境で使えるような設定をしてくれます。
便利ですね。

$ amplify add hosting
  ? Select the environment setup: DEV (S3 only with HTTP)
  ? hosting bucket name amplify-form-xxxxxxxxxxxxx-hostingbucket
  ? index doc for the website index.html
  ? error doc for the website index.html
  
$ npm run build

$ amplify publish

s3のurlがわからなくなったら下記コマンドで確認可能です。

$ amplify status

Current Environment: prod

| Category | Resource name   | Operation | Provider plugin   |
| -------- | --------------- | --------- | ----------------- |
| Function | amplifyFormAPI  | No Change | awscloudformation |
| Api      | amplifyFormAPI  | No Change | awscloudformation |
| Hosting  | S3AndCloudFront | No Change | awscloudformation |

Hosting endpoint: http://amplify-form-xxxxxxxxxxxxx-hostingbucket-prod.s3-website-ap-northeast-1.amazonaws.com

これでフロントエンド、バックエンド共に完成しましたね。urlにアクセスして、お問い合わせを送信してみるとうまく動くことが確認できますね。

メールの確認をしてみるとADMIN_EMAILで設定した値にメールが届いていることも確認できます。

6. 片付け

コマンド1つでリソースの削除が可能です。

$ amplify delete
? Are you sure you want to continue?(This would delete all the environments of the project from the cloud and wipe out all the local amplify resource files) Yes

Deleting env:prod
⠋ Deleting resources from the cloud. This may take a few minutes...Profile oku is configured to assume role
  arn:aws:iam::111111111111:role/cm-oku.takuya
It requires MFA authentication. The MFA device is
  arn:aws:iam::111111111111:mfa/cm-oku.takuya
? Enter the MFA token code: 111111
⠋ Deleting resources from the cloud. This may take a few minutes...

さいごに

今回はLambdaを触ったのでバックエンドも触ってしまいましたが、フロントエンド + CLI で簡単なインフラが構築できるのは魅力的ですね。
今後もどんどんAmplifyが便利になればいいなと思います。