技術メモ

プログラミングとか電子工作とか

リモートワーク蔓延る世の中になったので、改めてRedmineを導入してみる

f:id:ysmn_deus:20200503142752p:plain

学生の頃に団体でのタスクを管理するためにRedmineを導入していたのですが、正直活用しきれていなかったのとPassengerのエラーにおびえていたので今まで前向きにRedmineの活用を検討してきませんでした。
(正直作業も一人で完結することが多いので・・・)
しかし、昨今の情勢に有効活用できる対象の方々が増えてるんじゃないかと思い、防備録を兼ねて記事にしてみます。

そもそもRedmineとは?

もしかしたらRedmineを知らずに見て下さっている方の為にざっくりと説明しておきますと、Redmineとはブラウザ上で利用できるプロジェクト管理アプリケーションです。
具体的な説明などは公式サイトを一読して頂ければ有り難いです。

redmine.jp

デモなんかもあります

my.redmine.jp

どういう人におすすめ?

基本的に複数人で一つのプロジェクトを進める人は活用を検討するのが良さそうです。

一人での進捗管理にも使えるかもしれませんが、Redmine特有の癖というか運用に対して色々と学ばねばならない点がありますので、正直面倒だと思います。
(本記事では余り言及しない予定です)

導入

前提

基本的に

  • 複数人で使うので外部からアクセスできるサーバーにセットアップ
  • 小規模(2~10人程度?)のアクセスを想定

という想定でセットアップします。
小規模なので今回はAWSのLightsailを選定しました。
中規模や転送量が多い場合はさくらのVPSなどもいいかもしれませんが、外向けの転送量は1TBまでは込みで3.5ドル(初月は無料)なので試してみる分にはワンコイン以下で運用できると思います。

サーバーのセットアップ

LightsailにはBitnamiによるセットアップが可能となっています。
本来であればデータベースなどセットアップがいるんですが、この機能を利用することで基本的なセットアップに関しては何も考えなくてもよくなります。

※ ただし、2021年4月以降も含む長期運用まで考えている方はLightsailのBitnamiでセットアップされる環境がUbuntuの16.04なのでバックアップとリストアの方法も合わせて確認しておいた方が良いかもしれません。

Lightsailでインスタンスの作成

Lightsailのコンソールから「インスタンスの作成」からインスタンスの作成画面に映ります。

f:id:ysmn_deus:20200502183720p:plain

「設計図の選択」のところで「Redmine」を選択します。
ロケーションは日本の方を想定しているのでデフォルトの東京リージョンから変更してません。

f:id:ysmn_deus:20200502184215p:plain

初めてLightsailを利用する方はSSHキーペアが設定されていないと思いますので新規作成して下さい。

f:id:ysmn_deus:20200502184418p:plain

インスタンスプランは一番最小の3.5USDのプランを選択してます。
リソース名は分かりやすい名前を設定しておいて下さい。(よく分からない人はデフォルトの「Redmine-1」で問題無いかとおもいます。)

f:id:ysmn_deus:20200502184623p:plain

これで「インスタンスの作成」をすると、基本的なセットアップが全て完了します。
なんと便利な・・・

f:id:ysmn_deus:20200502185038p:plain

ホームに先ほど設定したリソース名のインスタンスが作成されていると思います。

IPの固定

今のままだとサーバーのIPが変わって鬱陶しいです。
IPを固定してドメインを割り当てる(割り当てなくても利用はできますが)場合にはIPを固定しておく必要があります。
LightsailでDNSゾーンを作成してドメインを割り当てる方法もありますが、今回DNSの設定は別途することとしてIPだけ固定します。

ホーム画面から作成したインスタンスの設定画面にいきます。

f:id:ysmn_deus:20200502190251p:plain

ネットワーキングのタブに移動します。

f:id:ysmn_deus:20200502190605p:plain

IPアドレス」と書いてある項目から「静的IPのアタッチ」を選択します。

f:id:ysmn_deus:20200502190905p:plain

他に静的IPのリソースが余っていない場合は「静的IPアドレスの作成」画面へ移動すると思います。
ロケーションはそのままで、インスタンスは先ほど設定したリソース名(ここでは「Redmine-1」)を選択して、「静的IPの指定」と書かれた項目のリソース名は分かりやすい名前を付けておきます。
画像では「StaticIp-1」となっていますが、「Redmine-1-StaticIp」などRedmine-1に利用している事を明示しておくのが良いと思います。

f:id:ysmn_deus:20200502191238p:plain

「作成」を選択すると静的IPが作成完了します。
とりあえずホームに戻り、先ほどと同様「Redmine-1」の設定画面の「ネットワーキング」を確認します。

f:id:ysmn_deus:20200502191521p:plain

画像のように「静的IPのデタッチ」が表示されていれば、IPの固定は完了しています。

IPへドメインを設定する

ドメインを何で運用しているかによりますが、基本的にDNSレコードにAレコードとして該当するIPを設定すると良いかと思います。
自分はRoute53を利用していますが、基本的になんでもいいとは思います。
(要望がありましたら記事化しますが、ここでは割愛します。)

基本的にHTTPSでの運用を考えてない人はココまでで問題ないかと思います。
(基本設定は公式サイトのドキュメントなどをご参照ください。)

redmine.jp

Redmineのセットアップ

管理者ユーザーでログイン

とりあえずHTTPS化したいのでその準備をします。
まずは管理ユーザでログインします。ドメインを指定した場合はそのドメインに、指定していない場合は http://[固定したIPアドレス]/ にアクセスして下さい。

f:id:ysmn_deus:20200503134438p:plain

たぶんこんな画面が表示されると思います。
右上の「ログイン」からログインします。
上記のようにBitnamiの機能でセットアップした場合は、ログインIDは「user」、パスワードはSSHでログインしたホームディレクトリにある"bitnami_application_password"というファイルに記載されているものを利用します。
Windows10であれば、コマンドプロンプトからSSHが使えますので、下記で取得可能です。
(キーペアはユーザーフォルダC:\Users\ユーザー名の直下の.sshというディレクトリに保存したとします。)

C:\Users\username> ssh bitnami@[設定したドメインor固定したIPアドレス] -i .\.ssh\[保存したキーペア].pem
$ cat bitnami_application_password

取得したパスワードでログインします。

f:id:ysmn_deus:20200503135614p:plain

ログインが完了すると、左上の項目に「管理」などが追加されています。
この「管理」を選択し、管理画面を表示します。

f:id:ysmn_deus:20200503135733p:plain

管理画面の「設定」を選択します。

f:id:ysmn_deus:20200503135944p:plain

この画面の「プロトコル」をHTTPからHTTPSにしておきます。

Bitnami HTTPS Configuration Toolを利用したHTTPS化

勿論この設定だけでHTTPS運用できるわけではないです。今回はLet's Encryptを活用したSSLを想定していますが、一応AWS Certificate ManagerとAmazon CloudFrontを利用した運用もできると思います。AWS信者の自分としては後者の方が安心安全な気がしますが、極力ミニマムに始めるという想定でLightsail上で完結する方法を採っています。

基本的なツールはなんと最初から入っており、一発で自動更新コマンドまで追加してくれます。
SSHでセットアップしたサーバーにログインしてコマンドを実行します。

sudo /opt/bitnami/bncert-tool

最初にドメイン名を聞かれるのでSSL化したいドメイン名を入力します。

Domain list []: redmine.hogefuga.com

次にwwwの扱いをどうするか(マルチドメインとして運用するのか否か)を聞かれるのでnでキャンセルしておきます。
(あんまりwww.redmine.hogefuga.comのような運用をしたい人はいないと思いますので)

The following domains were not included: www.hogefuga.com. Do you want to add them? [Y/n]: n

次にHTTPへのアクセスがあった場合にHTTPSにリダイレクトするか聞かれますので、yで許諾しておきます。

Enable HTTP to HTTPS redirection [Y/n]: y

次に設定のステップが表示され「これでええんか?」と聞かれますのでyで許諾しておきます。

Do you agree to these changes? [Y/n]: y

次に期限切れになりそうなときに通知が来るメールアドレスを登録しておきます。

E-mail address []: hoge@hogefuga.com

ここまで来たら基本の設定は完了です。最後に利用規約に同意するか否か聞かれるのでyで許諾します。

Do you agree to the Let's Encrypt Subscriber Agreement? [Y/n]: y

これでSSL化は完了です。
このツールは基本的に自動更新のコマンドも登録してくれています。一応確認する場合は、そのまま

crontab -e

とcronを確認するとコマンドが追加されていることが分かります。

オンラインでの外部とのやりとりが増えてきたので、GW中にチケット駆動など開発手法を学習しておきたいところです。
Redmineの運用に関してはド素人なので・・・(:3 」∠ )

3Dプリンタでマスクを作る「PITATT 3D print mask」を試してみた

普段はAirinum Urban Air Maskを利用しているのですが、正直フィルターが枯渇しそうです。
6月ぐらいに在庫復活する、と公式サイトに記載がありますがあやしいところ。
そこで、3Dプリンタで出力可能なマスク「PITATT 3D print mask」が公開されているという事で、これを試してみることに。

公式ウェブサイトからSTLファイル(インナーとアウターの2つのデータ)をダウンロードしてきて、スライサーで処理して出力するだけ!簡単!
と思っていましたが、アウターの出力がどうしてもサポート無しだと最後の方の積層で失敗するのでサポートありで出力しました。
(フィラメントは三菱ケミカルメディア Verbatimの白PLAを利用しました。今は白はないみたいです。)

f:id:ysmn_deus:20200418161431j:plain
実際に出力したマスク

このマスクは2個の部品に分かれており、インナーを外すことでフィルターを交換することができます。

f:id:ysmn_deus:20200418161609j:plain

フィルターはティッシュやガーゼを使って下さいとのこと。
この辺ならまだ手に入るのでありがたいです。

想像してたよりきっちり密着します。
相当試行錯誤したんではないでしょうか?頭が下がります。

医療従事者や専門職の方には不適だと思いますが、それ以外の方の応急対応としてはありだと思います。
コレを機に3Dプリンタを購入してみたいな~なんて思う人は、少なくとも150mm x 100mm x 100mm の造形エリアが確保できる3Dプリンタを購入する事をオススメします。
(最近は造形エリア広いのでだいたい大丈夫だと思いますが)

AWS Lambdaのローカル開発で環境変数を取り扱うとき(SAM テンプレート利用)

f:id:ysmn_deus:20190318182949p:plain

Lambdaで他サービスのAPIを度々利用したくなると思います。
(例えば、S3にアクセスしたり、DynamoDBからデータを取り出したり等)
ローカルでは権限を設定したIAMユーザーの認証情報を利用して実行し、運用時にはロールをLambdaに設定して走らせたいところです。
TypeScriptのwebpackでのコンパイル分岐の為にdotenvを利用しているので、そこに設定してもいいんですが、SAMのテンプレートに記述する方法もあるのでそちらをまとめておきます。

テンプレートを分ける

開発環境と運用環境などを分けたくなるので、テンプレートと環境変数ファイルを複数用意したいです。
なのでディレクトリ(もしくはファイル)で分けておきます。
具体的には

プロジェクトディレクト
├ sam/
│├ develop/ ←開発用ディレクト
││├ env.json
││└ template.yaml
│├ test/ ←自動テスト用ディレクト
││├ env.json
││└ template.yaml
│└ deploy/ ←運用版ディレクト
│ ├ env.json
│ └ template.yaml
├ built/ ←TypeScriptのコンパイル
├・・・

のようなイメージです。
(全てに環境変数が要るかは状況次第だとは思いますが)
これでとりあえず.gitignore**/env.jsonとでも追記しておけばリポジトリにヤバイ情報をアップロードしなくて済みそうです。

テンプレートの修正

ここで、SAMのテンプレートファイルを修正しておきます。
テンプレートのResources関数名PropertiesCodeUriは、テンプレートからみた相対パスなので、上記のディレクトリ構成を取っている場合は

~
    Properties:
      CodeUri: ../../built/
~

となります。

試しにローカル実行してみる

とりあえず色々動かしたのでローカルで実行してみます。
テンプレートを何個か使い分けるので、プロジェクトのルートディレクトリからテンプレートを明示して実行します。

sam local start-api -p 3001 -t sam/develop/template.yaml

今回はポート(3001)も明示しました。(よくReactで3000番は使ってるので)
とりあえず今は動けばOKです。

環境変数の定義

早速環境変数を設定していきます。
SAMでローカル実行する際に環境変数を使うには

  1. テンプレート上で変数を定義する
  2. 読み込むJSONを作成する

という手順が必要です。
コマンドですんなり入ってくれればいいんですが、テンプレート上で変数を定義するということをお忘れ無きよう。

テンプレート上で変数を定義する

テンプレートに変数の情報を書き込んでいきます。
環境変数の適応には2通りあり

  • Globalsに記述してResources全てで適応できる変数を定義する
  • Resourcesの関数毎に変数を定義する

のどちらかになります。
自分の記憶では、以前は両方を両立できたと思うんですが、現状どちらかを選べば片方の環境変数がうまく注入されないような気がします。
(例えば、Globalsに環境変数を定義すると、Resourcesの環境変数がうまく変更できない)
基本的には公式ドキュメントで例のある後者をオススメします。

Globalsに記述してResources全てで適応できる変数を定義する

テンプレートには下記の様に記載します。

~
Globals:
  Function:
    Runtime: nodejs10.x
    Timeout: 3
    Environment:
      Variables:
        GLOBAL_ENV: DUMMY
~

注入する環境変数JSONファイルは下記のように作成します。

{
  "Parameters": {
    "GLOBAL_ENV": "hoge"
  }
}

Resourcesの関数毎に変数を定義する

テンプレートには下記の様に記載します。

~
Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: ../../built/
      Handler: development.lambdaHandler
      Events:
        HelloWorld:
          Type: Api
          Properties:
            Path: /hello
            Method: get
      Environment:
        Variables:
          FUNC_ENV: DUMMY
~

注入する環境変数JSONファイルは下記のように作成します。

{
  "HelloWorldFunction": {
    "FUNC_ENV": "fuga"
  }
}

環境変数を注入して実行

上記の2通りどちらでも実行方法は同じです。

sam local start-api -p 3001 -t sam/develop/template.yaml -n sam/develop/env.json

上記のコマンドをpackage.jsonに書いておけば、簡単にテストできます。
自分はnpm-run-allを利用してSAMのローカルサーバーとwebpackのwatchを走らせてますのでpackage.jsonscripts

~
  "scripts": {
    "dev:localhost": "sam local start-api -p 3001 -t sam/develop/template.yaml -n sam/develop/env.json",
    "dev:watch": "webpack --watch",
    "dev": "run-p dev:*",
~

としておけば、yarn dev でSAMとwebpackが走ります。

AWS LambdaをTypeScriptで開発する

f:id:ysmn_deus:20190318182949p:plain

JavaScriptはオワコン!とまでは言いたくないんですが、やはり普段からTypeScriptを利用していると極力JavaScriptは避けたいところ。
ということで基本JavaScriptで開発されているであろうAWS LambdaのNode環境ですが、TypeScriptで開発を始めるまでをまとめておきます。
Serverless Frameworkを使えば比較的容易にTypeScript環境を構築できるようですが、またそれは別途試してみます。
今回はSAM CLIで対応します。
(デプロイはJavaScriptと変わらないので割愛します)

SAM開発環境を整える

SAM CLIのインストール

まずはSAM CLIをインストールする必要があります。
Dockerのインストールも必要になってきますが、基本的には公式ドキュメント(Installing the AWS SAM CLI)に一通り書いてあるのでそちらを参照して下さい。

最終的に

sam --version

がきちんと動作すれば問題ないと思います。

(補足)IAMユーザー情報の登録

AWS CLIを普段利用していない方などはIAMユーザーの認証情報をPCにセットアップする必要があります。
AWSを利用していてLambdaだけしか使わない人も珍しいと思いますので、詳細は割愛しますが公式ドキュメント(Setting Up AWS Credentials)AWS CLIを利用しない認証情報のセットアップも掲載されておりますのでご参照ください。
個人的にはAWS CLIをセットアップすることをオススメしますが。

SAM CLIでプロジェクトを作成する

SAMでプロジェクトを作成します。
とりあえずはHello World的なものを生成する所まではJavaScriptで対応します。

sam init --runtime nodejs10.x --name hello-sam

上記コマンドのオプションをとりあえず説明しておきますと、
--runtime:Lambdaの実行ランタイムを選択します。(python3.7とか)ホントはv12を使っていきたいんですが、一応安牌をとってv10で進めて行きます。
--name:プロジェクトのディレクトリ名です。Lambdaの関数名などは別途修正する必要があります。

上記コマンドを実行すると対話形式でウィザードが進みます。
とりあえず今回は1, 1をチョイスしました。

Which template source would you like to use?
        1 - AWS Quick Start Templates
        2 - Custom Template Location
Choice: 1

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

AWS quick start application templates:
        1 - Hello World Example
        2 - Quick Start: From Scratch
        3 - Quick Start: Scheduled Events
        4 - Quick Start: S3
        5 - Quick Start: SNS
        6 - Quick Start: SQS
        7 - Quick Start: Web Backend
Template selection: 1

-----------------------
Generating application:
-----------------------
Name: hello-sam
Runtime: nodejs10.x
Dependency Manager: npm
Application Template: hello-world
Output Directory: .

Next steps can be found in the README file at ./hello-sam/README.md

S3などの処理を書きたいときはその辺を選んだりするのが良いんでしょう。
とりあえず今回は無難なチョイスを選択しましたが、ウィザードが完了すると--nameで指定したディレクトリにファイルが出力されていると思います。

テストを実行する

SAM CLIでセットアップすると、mocha+chaiがセットアップできるようになっています。とりあえず走らせる為にpackage.jsonの情報でセットアップします。

cd .\hello-sam\
cd .\hello-world\
yarn

ドキュメントではnpmコマンドを利用している例が多いですが普段はyarnを利用しているのでそのまま使います。
基本問題無いのでそのままテストを走らせます。

yarn test

yarn run v1.22.0
$ mocha tests/unit/


  Tests index
    √ verifies successful response


  1 passing (7ms)

Done in 0.34s.

問題無さそうです。

ローカルで関数を実行する

SAMを利用すればローカルで関数を実行するのは非常に簡単です。
一応sam local invokeで関数を実行する方法と、sam local start-apiで実行する2種類の方法がありますが、大体後者かと思いますので後者で動かしてみます。
(Dockerが起動してないと怒られますので起動しておきましょう)

cd ..
sam local start-api

問題が無ければブラウザでhttp://127.0.0.1:3000/helloを入力するか、curlコマンドでGETリクエストを投げると{"message":"hello world"}が返ってきます。
基本的にJavaScriptのファイル群はここで不要となりますので、hello-worldディレクトリはこの段階で削除してしまって構いません。

ちなみに、Dockerをセットアップしただけの状態だとC:\Users以下以外のディレクトリは権限がなくてマウントされないので、プロジェクトのディレクトリがC:\Users以下でない場合はsam local Error: Cannot find module 'app' docker toolboxのようなエラーが発生します。
対処法としましてはDokcer Toolboxでは下記のサイトを参考にさせていただきました。

coffee-nominagara.com

ToolboxでないDokcerでは警告が出てきちんと設定していた気がします。

TypeScript

tsconfig.jsonの作成

まずはプロジェクトのルートディレクトリにtsconfig.jsonを作っておきます。
特にこだわりのない人は

tsc --init

tsconfig.jsonを作成するのが良いと思います。
一応今回用意しているtsconfig.jsonは下記の通りにしました。

{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es6",
    "noImplicitAny": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "outDir": "./built",
    "allowJs": true,
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "esModuleInterop": true
  },
  "include": [
    "./src/**/*"
  ],
    "exclude": ["node_modules"]
}

入力対象を./src/以下、出力先を./builtにしています。

パッケージを追加

TypeScriptを使っているので型が必要です。開発用の型をまずは追加していきます。

yarn add -D @types/aws-lambda

最小限のパッケージはこれだけでOKです。

index.tsを編集

index.tsを編集します。基本的にはデフォルトのapp.jsを踏襲して作成してみます。

import {APIGatewayEvent, Context} from "aws-lambda"

const lambdaHandler = async (event: APIGatewayEvent, context: Context) => {
    let response: any
    try {
        response = {
            'statusCode': 200,
            'body': JSON.stringify({
                message: 'hello ts world',
            })
        }
    } catch (err) {
        console.log(err)
        return err
    }

    return response
}

export {lambdaHandler}

とりあえずという意味ではeventcontextanyで受けてしまえばいいんですが、まぁそんぐらいは・・・
これでコンパイルしてみます。tsconfig.jsonを設定してるのでルートディレクトリでtscコマンドを実行します。

tsc

これでbuiltディレクトリが作成され、index.jsが生成されたかと思います。

template.yamlの修正

JavaScriptの時との差異はコンパイル後のファイル名ファイルの場所です。
これをtemplate.yamlに修正していきます。

~
Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: built/
      Handler: index.lambdaHandler
~

CodeUriHandlerを修正しました。

ローカルで関数を実行する

これでローカル関数を実行してみます。

sam local start-api

JavaScriptの時と同様にブラウザかcurlコマンドで確認してみて下さい。
返ってくるのが{"message":"hello world"}のままの人はもしかするとsam buildして.aws-samディレクトリに履歴が残ってる可能性がありますので消しちゃって下さい。
無事{"message":"hello ts world"}が返ってきたら最低限度の環境構築はできたのではないでしょうか。

その他

ESLint + Prettier

基本的にはReactの設定と同じなのですが、ReactのJSXなどは不要です。
一応eslintrcなどを掲載しておきます。

パッケージの追加

yarn add -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
yarn add -D prettier @types/prettier @types/eslint-plugin-prettier eslint-config-prettier eslint-plugin-prettier

eslintrc.js

module.exports = {
  env: {
    browser: true,
    node: true,
    es6: true
  },
  parser: '@typescript-eslint/parser',
  parserOptions: {
    sourceType: "module"
  },
  plugins: [
    'prettier',
    '@typescript-eslint'
  ],
  extends: [
    "eslint:recommended",
    'plugin:@typescript-eslint/recommended',
    'plugin:prettier/recommended',
    'prettier/@typescript-eslint'
  ],
  rules: {
    'prettier/prettier': [
      'error',
      {
        'semi': false,
        'trailingComma': "all"
      }
    ]
  },
};

.eslintignore

node_modules/
*.config.js
built/

webpack

今回はwebpack+ts-loaderで構成します。

パッケージの追加

yarn add -D ts-loader webpack webpack-cli dotenv

今回は.env環境変数を管理する想定でdotenvを利用しています。
直で書いたりする場合はこの辺は必要無いと思います。
とりあえず.envを作っておきます。

NODE_ENV=development

webpack.config.js

const path = require("path")
const dotenv = require("dotenv")
dotenv.config()

const BUILT_PATH = path.resolve(__dirname, "./built")
const BUILD_VARIANT = process.env.NODE_ENV

module.exports = {
  target: "node",
  mode: BUILD_VARIANT === "production" ? "production" : "development",
  resolve: {
    extensions: [".ts", ".js"],
  },
  entry: "./src/index.ts",
  output: {
    filename: `${BUILD_VARIANT}.js`,
    path: BUILT_PATH,
    library: "[name]",
    libraryTarget: "commonjs2",
  },
  module: {
    rules: [
      {
        test: /\.ts$/,
        loader: "ts-loader",
      },
    ],
  },
}

これで、ファイルを分割して開発することができます。

React Hook Form入門 その2(基本編)

f:id:ysmn_deus:20200224171811p:plain

前回の続きです。

基本的な使い方

バリデーションなどを無視するなら、基本的にはフォームの要素のref属性にuseFormから得られるregister関数を登録し、formのonSubmithandleSubmitを登録するだけで利用できます。
あえてフォームを作る順序を書くなら

  1. フォームの要素を書き出す
  2. フィールドを登録する
  3. スタイリング(ここでは取り扱わない)

という流れになるかと思います。最初から順にやっていきます。

フォームの要素を書き出す

基本的には普通のHTMLとなんら変わり無い要素をReactで表現します。

import React from "react"

export default function App() {
  return (
    <form>
      <input name="firstName" />
      <select name="gender">
        <option value="male">male</option>
        <option value="female">female</option>
      </select>
      <input type="submit" />
    </form>
  )
}

f:id:ysmn_deus:20200224173934p:plain

非常にシンプルなフォームですが、今回はこれに機能を実装していきます。

フィールドを登録する

ここでは

  • input(name)
  • select(gender)

という2種類のフォームの要素があります。これらをReact Hook Formのフィールドに登録していきます。

import React from "react"
import { useForm } from "react-hook-form" // useFormをreact-hook-formからインポート

export default function App() {
  const { register, handleSubmit } = useForm() // register, handleSubmitをuseFormというHooksから得る
  const onSubmit = (data: any) => console.log(data) // フォームが送信されたときに実行される関数

  return (
    // form要素にhandleSubmitを渡す。なお、引数にはフォーム送信時に実行される関数を渡す。
    <form onSubmit={handleSubmit(onSubmit)}>
      {/* input 要素にref属性を付与、registerを渡す*/}
      <input name="firstName" ref={register} />
      {/* select 要素にref属性を付与、registerを渡す*/}
      <select name="gender" ref={register}>
        <option value="male">male</option>
        <option value="female">female</option>
      </select>
      <input type="submit" />
    </form>
  )
}

基本的にはこれだけでフォームが利用可能です。
フォームのデータを利用してなにがしかしたいときはonSubmitの中に処理を書いていきます。

心がTSに支配されてる方は(data: any)で発狂しそうかと思いますが、後で言及するので我慢して下さい。僕も発狂寸前なんです。

非同期処理

おそらくフォームの送信の後にはAPIと通信したりするので非同期処理が来ると思います。
上記のhandleSubmitの引数にはPromiseも渡せるので、async関数がそのまま渡せます。

import React from "react"
import { useForm } from "react-hook-form"

const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))

export default function App() {
  const { register, handleSubmit } = useForm()
  const onSubmit = async (data: any) => {
    console.log(data)
    await sleep(1000)
    console.log("complete.")
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input name="firstName" ref={register} />
      <select name="gender" ref={register}>
        <option value="male">male</option>
        <option value="female">female</option>
      </select>
      <input type="submit" />
    </form>
  )
}

バリデーションを適応する

基本的には上記までで良さそうなものですが、バリデーションぐらいはフロントでもやっておくれ、という気持ちが湧いてきます。
React Hooks FormはHTML 標準のフォームバリデーションのバリデーションには対応しているようです。
対応しているルールは公式サイトを閲覧してもらえればと思いますが、大雑把に言えば

などのルールを適応し、バリデーションに通らない場合は即座にエラーを得ることができるようです。

import React from "react"
import { useForm } from "react-hook-form"

export default function App() {
  const { register, handleSubmit, errors } = useForm()
  const onSubmit = (data: any) => console.log(data)

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input name="firstName" ref={register({ required: true })} />
      {errors.firstName && "First name is required"}
      <input name="lastName" ref={register({ pattern: /^[A-Za-z]+$/i })} />
      {errors.lastName && "Last name is not mached [A-Za-z]"}
      <input name="age" type="number" ref={register({ min: 18, max: 99 })} />
      {errors.age && "age is in ragnge 18-99"}
      <input type="submit" />
    </form>
  )
}

errors.の後にname属性で決めたプロパティを参照するとバリデーションのルールを適応した可否が得られます。
上記の例ではboolean &&の形で、もしバリデーションが通ってなければ警告文を表示する様な形式にしています。
バリデーションに適合していなければ、送信してもonSubmitが実行されません。

UIライブラリの利用

一応方法としては

  1. 対応するUIライブラリ(material-uiなど)を利用する
  2. Controllerを利用してカスタム登録処理をする
  3. useEffectを利用する

の3種類ありますが、自分は基本的にmaterial-uiしか利用しないので上記2点にのみ言及します。

対応するUIライブラリを利用する

これが最も簡単です。material-uiを利用している人は、属性を追加するだけで利用できます。
まずはmaterial-uiをインストールします。

yarn add @material-ui/core

もうこれでフォームが作成可能です。
ソースを変更していきます。

import React from "react"
import { useForm } from "react-hook-form"
import TextField from "@material-ui/core/TextField"

export default function App() {
  const { register, handleSubmit } = useForm()
  const onSubmit = (data: any) => console.log(data)

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <TextField inputRef={register} label="First name" name="firstName" />
      <input type="submit" />
    </form>
  )
}

これで、TextFieldコンポーネントに入力された値がonSubmitdataの中に放り込まれます。

Controllerを利用してカスタム登録処理をする

テキストフィールドやチェックボックスぐらいであれば上記の処理で問題無いんですが、ラジオボタンなどのフォーム要素が制御されたもの等を利用する場合は上記の用にはいきません。
例えば、ラジオボタンの例を出せば

import React, { useState } from "react"
import { useForm } from "react-hook-form"
import {
  TextField,
  FormControl,
  FormLabel,
  FormControlLabel,
  Radio,
  RadioGroup,
} from "@material-ui/core"

export default function App() {
  const { register, handleSubmit } = useForm()
  const onSubmit = (data: any) => console.log(data)
  const radioList: string[] = ["male", "female", "other"]
  const [value, setValue] = useState(radioList[0])
  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setValue((event.target as HTMLInputElement).value)
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <TextField inputRef={register} label="First name" name="firstName" />
      <FormControl>
        <FormLabel>Gender</FormLabel>
        <RadioGroup name="gender1" value={value} onChange={handleChange}>
          {radioList.map(item => (
            <FormControlLabel
              key={item}
              value={item}
              control={<Radio />}
              label={item}
            />
          ))}
        </RadioGroup>
      </FormControl>
      <input type="submit" />
    </form>
  )
}

といった場合にはinputRefregisterを登録すればOKという訳にもいきません。
そこで、React Hook FormにはControllerというものが用意されています。

import React from "react"
import { useForm, Controller } from "react-hook-form"
import {
  TextField,
  FormLabel,
  FormControlLabel,
  Radio,
  RadioGroup,
} from "@material-ui/core"

const radioList: string[] = ["male", "female", "other"]

export default function App() {
  const { register, handleSubmit, control } = useForm()
  const onSubmit = (data: any) => console.log(data)

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <TextField inputRef={register} label="First name" name="firstName" />
      {/* 上記と同じ、Controllerを利用してTextFieldを使い場合の例 */}
      <Controller
        name="firstName2"
        as={TextField}
        control={control}
        defaultValue=""
      />
      {/* FormControlのセクションを<div></div>と想定 */}
      <div>
        <FormLabel>Gender</FormLabel>
        <Controller
          name="radioGroup"
          as={
            <RadioGroup name="gender1">
              {radioList.map(item => (
                <FormControlLabel
                  key={item}
                  value={item}
                  control={<Radio />}
                  label={item}
                />
              ))}
            </RadioGroup>
          }
          control={control}
          defaultValue={radioList[0]}
        />
      </div>

      <input type="submit" />
    </form>
  )
}

やや複雑になりましたが、<FormControl></FormControl>の例と比べても、そこまで記述量が増えていないにもかかわらずフォームデータのやりとりができるのが確認できます。
なお、ここではしれっとdefaultValueを指定していますが、こいつを指定していないとMaterial-UIはコンパイル時にエラーを吐きます。
(たぶんa component is changing an uncontrolled radiogroup to be controlled react hook formみたいなやつ)
フォームの要素を記述する時にdefaultValue={}として値を指定するか、useFormの歳に引数に初期値を入力するかのどちらかで対応して下さい。

TypeScriptの対応

上記までは、onSubmitにany型をたたき込んだりとTypeScriptの良さを出し切れてない使い方でしたが、useFormに型を適応することでuseFormから得られるsetValueerrorsに型を適応することができます。

import * as React from "react"
import { useForm } from "react-hook-form"

// フォームにあるフィールドの型の定義
type FormData = {
  firstName: string
  lastName: string
}

export default function App() {
  const { register, setValue, handleSubmit, errors } = useForm<FormData>() // フィールドの型定義をuseFormに適応する
  // handleSubmitの引数にはフィールドの型定義が適応されている
  const onSubmit = handleSubmit(({ firstName, lastName }) => {
    console.log(firstName, lastName)
  })

  return (
    <form onSubmit={onSubmit}>
      <label>First Name</label>
      <input name="firstName" ref={register} />
      <label>Last Name</label>
      <input name="lastName" ref={register} />
      <button
        type="button"
        onClick={() => {
          // setValueにも型定義が適応されている
          setValue("lastName", "luo") // OK
          setValue("firstName", true) // コンパイルエラー
          errors.bill // errorsにも型定義が適応されているのでコンパイルエラー
        }}
      >
        SetValue
      </button>
    </form>
  )
}

React Hook Form入門 その1(導入編)

f:id:ysmn_deus:20200224171811p:plain

去年はNuxt.jsやNext.jsなど色々触ってたんですが、結局プレーンなReactが扱いやすいんじゃないかと思い色々試行錯誤しています。(Nextは結構良い感じだったんですが、Amplifyとの相性が悪くて不採用となりました)
基本的にはReact + Reduxの組み合わせが便利だと思い利用していたのですが、極力React Hooksを利用した方がシンプルな状態管理ができそうなので(きちんと理解してないのでレンダリングのタイミングでStateを変更してしまったり・・・)フォームはRedux FormからReact Hook Formに移行しようと思います。

この記事ではReact Hook Formに慣れるためにアプリケーションを作成しながら基本的な機能を確認していきます。

初期設定

Reactのプロジェクト作成(create-react-app)

プロジェクトを作成します。この辺は基本的なReactのプロジェクト作成と同じで、create-react-appを利用します。

create-react-app . --template typescript

(コマンドはプロジェクト用のディレクトリ内で実行してます。)

とりあえず実行

まずはプロジェクトとして問題無いことを確認しておきましょう。
基本的にパッケージマネージャーはyarnを利用します。

yarn start

f:id:ysmn_deus:20200224133318p:plain
いつもの

プロジェクトとしてはOKです。
一応ガッツリ開発するわけではないですが、ESLint + Prettierの設定なども個人的によく忘れるので行っておきます。

ESLint + Prettier

ESLintのインストール

まずはESLintをインストールしていきます。
TypeScriptでの開発を想定しているので、@typescript-eslint/parser@typescript-eslint/eslint-pluginも合わせてインストールします。

yarn add -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin

ESLintの設定

プロジェクトのルートディレクトリ(create-react-appした場所)に.eslintrc.jsを作成します。

module.exports = {
    env: {
        browser: true,
        node: true,
        es6: true
    },
    parser: '@typescript-eslint/parser',
    parserOptions: {
        ecmaFeatures: {
            jsx: true
        },
        sourceType: "module"
    },
    plugins: [
        "react",
        '@typescript-eslint'
    ],
    extends: [
        "eslint:recommended",
        "plugin:react/recommended",
        'plugin:@typescript-eslint/recommended',
    ],
    rules: {
    },
    overrides: [
        {
            'files': ["**/*.tsx"],
            'rules': {
                'react/prop-types': 'off'
            }
        }
    ]
};

とりあえず動けばいいのでこんなもんで。
詳しい説明とかは今回は省きます。

package.jsonにLint用のコマンドを記述します。

~
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "lint": "eslint --ext .ts,.js,.tsx,.jsx --ignore-path .gitignore ."
  },
~

ここまでできたら、試しに実行します。

yarn lint

errorとwarningが色々出てくると思いますが、eslintが動いてる証拠なので問題無いです。

Prettierのインストール

ESLintの設定ができたら次はPrettierをインストールします。
ESLintとの競合などにも対応するためeslint-config-prettiereslint-plugin-prettierも合わせてインストールしておきます。

yarn add -D prettier @types/prettier @types/eslint-plugin-prettier eslint-config-prettier eslint-plugin-prettier

Prettierの設定

基本的にESLintの補助機能として利用するため、ESLintの設定に追記します。

module.exports = {
  env: {
    browser: true,
    node: true,
    es6: true
  },
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaFeatures: {
      jsx: true
    },
    sourceType: "module"
  },
  plugins: [
    'prettier',
    "react",
    '@typescript-eslint'
  ],
  extends: [
    "eslint:recommended",
    "plugin:react/recommended",
    'plugin:@typescript-eslint/recommended',
    'plugin:prettier/recommended',
    'prettier/@typescript-eslint'
  ],
  rules: {
    'prettier/prettier': [
      'error',
      {
        'semi': false,
        'trailingComma': "all"
      }
    ]
  },
  overrides: [
    {
      'files': ["**/*.tsx"],
      'rules': {
        'react/prop-types': 'off'
      }
    }
  ]
};

基本的に上記の設定で動くとは思います。
試しに実行してみましょう。

yarn lint

errorやwarningがでれば問題ありません。
おそらくこの後保存されれば自動でlintが走るようにすると思いますが、lintの設定で勝手に整形するには--fixオプションを付けます。
試しに付けて実行します。

yarn lint --fix

大部分が勝手に修正されました。
ここで、直らないファイルで、errorのものは

  • App.test.tsx
  • serviceWorker.ts

の二種類かと思います。
今回テストは書かないので、App.test.tsxは削除して対応しますが、testexpectが未定義というエラーなので、おそらくeslint-plugin-jestとか入れれば直ると思います。
serviceWorker.tsに関しては、基本触らないファイルなので、該当箇所のみ無視する様に修正します。

~
        checkValidServiceWorker(swUrl, config) // eslint-disable-line @typescript-eslint/no-use-before-define
~
        registerValidSW(swUrl, config) // eslint-disable-line @typescript-eslint/no-use-before-define
~

対応後にもう一度lintを走らせます。

yarn lint

errorは消えてwarningだけになったかと思います。
warningも気になる人は対応してください。(serviceWorker.tsはファイルごと無視してもいいかもしれません。)

ESLint + Prettierのその他の設定

ファイルウォッチャーに登録する(保存したら勝手に処理する)のは、開発環境によって異なるので省きます。

TypeScript

tsconfig.json

incrementalの設定とexcludeの設定を追記しておきます。

~
    "jsx": "react",
    "baseUrl": "src",
    "incremental": true
  },
  "exclude": [
    "node_modules"
  ],
  "include": [
~

React Hook Form

ようやく本題です。

インストール

インストールは至って簡単です。

yarn add react-hook-form

これで使用可能になります。

基本的な利用方法

試しにクイックスタートのサンプルを利用してみます。
基本的には書いてある通りでいいんですが、TypeScriptだとコンパイル時にエラーが出るので、余計なコメントなどを修正します。

import React from "react"
import { useForm } from "react-hook-form"

export default function App() {
  const { register, handleSubmit, watch, errors } = useForm()
  const onSubmit = (data: any) => {
    console.log(data)
  }

  console.log(watch("example"))

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input name="example" defaultValue="test" ref={register} />

      <input name="exampleRequired" ref={register({ required: true })} />
      {errors.exampleRequired && <span>This field is required</span>}

      <input type="submit" />
    </form>
  )
}

とりあえず使えるか実行して確認します。

yarn start

超シンプルですが、フォームが出てきました。

f:id:ysmn_deus:20200224144121p:plain

2個目の入力フォームが必須のようなので、適当に値を入れて「送信」を押します。

f:id:ysmn_deus:20200224144425p:plain

基本的にはformのonSubmituseFormというHookを利用して得たhandleSubmitという関数を通じて、handleSubmitに渡した関数がフォームのデータを引数に貰って実行されるようです。
また、watchという関数はフォームの変化を監視しているようで、testと記載されているフォームを編集するとconsole.log(watch('example'))が毎回発火します。

f:id:ysmn_deus:20200224144752p:plain

長くなったのでバリデーションとかはまた次回に。

EC2を使わないAWSでメール送信/受信(転送)(SES + S3 + Lambda)

f:id:ysmn_deus:20200215223446p:plain

Amazon WorkMail使えばええやんって所なんですが、無意味にメアドを増やしたい時なんかは 4.0 USD/Monthはちょい高め。
なので、勉強がてらSES、S3、Lambdaを使ったメール送受信を導入してみることにしました。
とはいえ、SESは本来送信専用なので受信はどこか普段使いのメールに転送する想定になります。

メール受信(転送)のセットアップ

まずはメールの受信から。
基本的にはAWSの公式ドキュメントを参照します。
https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-getting-started.html
AWSで痛い目に遭いたくなければ日本語ドキュメントは読まないことです。古事記にもそう書かれている。

Step 1: 初期設定

まずSESの設定とかを始める前にやっておくことが2点あります。

  • AWSのアカウントを作っておく
  • Route53にドメインを登録しておく

基本的にこれらはできてる想定で話を進めます。

Step 2: ドメインの認証

SESでドメインを利用するに当たってドメインの認証が必要になります。
とりあえずSESのコンソールに行きましょう。
https://console.aws.amazon.com/ses/

リージョンは東京がないのでとりあえずバージニア北部(us-east-1)を想定してます。
SESのコンソールを開いたら「Identity Managemen」の「Domains」に行きます。 f:id:ysmn_deus:20200214133008p:plain f:id:ysmn_deus:20200214130405p:plain

Verify a New Domain

なんもないと思うので、左上の「Verify a New Domain」をクリックします。
「Verify a New Domain」というダイアログが出てくると思うので、「Domain」にRoute53に登録したドメインを入力し、「Generate DKIM Setting」にチェックを付けて「Verify This Domain」をクリックします。
DKIMに関しては設定してなくても基本いけると思いますが、送信先によってははじかれたりするので設定しておくのが無難かな、と思います。

「Verify This Domain」をクリックすると、ダイアログの内容が少々変わってなんかでてくると思います。
f:id:ysmn_deus:20200214131648p:plain
Route53を利用しない場合は表示されているレコードをドメインに設定する必要がありますが、AWSに命を捧げている者はAWS様がよしなにしてくれます。
ドメインのレコードをRoute53を利用しない人はここで個別に設定が必要ですが、Route53ユーザーであれば迷わず「Use Route 53」をクリックしましょう。

Use Route 53

次に「Use Route 53」というダイアログが出てきます。
前の段落で設定する筈だった項目をウィザードでやってくれます。
f:id:ysmn_deus:20200214132315p:plain
初めてドメインにレコードを設定する人であれば特に気にせず「Email Receiving Record」の部分もチェック入れて良いと思いますが、もし他にメールアドレスを登録してる人などはMXレコードが上書きされてしまうので要注意です。
全てにチェックを入れて「Create Record Sets」をクリックします。
するとSESのコンソールに戻り、設定していたドメインがpendingになってるのが分かります。
f:id:ysmn_deus:20200214132724p:plain
ここで5分ほど待ちます。待ってる間にRoute53で設定したドメインのレコードに先ほどのウィザードで設定したレコードが追加されているか確認してもいいと思います。

時間がたったらたぶんverifiedになってると思います。
f:id:ysmn_deus:20200214133008p:plain

Step 3: 受信ルールの作成

Step2でドメインの認証を済ませたら、受信設定を作成します。
SESのコンソールの「Email Receiving」の項目から「Rule Sets」をクリックし、「Create a Receipt Rule」をクリックしましょう。
f:id:ysmn_deus:20200214133617p:plain

受信相手の設定

最初に出てくるのは受信対象の設定です。
もし受信するドメインを限定したりする場合はこの辺を設定しましょう。
たぶん設定する人は少ないと思うので、特になにも入力せず「Next Step」をクリックしましょう。
f:id:ysmn_deus:20200214133857p:plain

保存先の設定(Actionの設定)

メールが来たときにそのメールをどうするかの挙動(Action)を設定します。
「Add action」から「S3」を選びます。
f:id:ysmn_deus:20200214134325p:plain
f:id:ysmn_deus:20200214134336p:plain

まずはS3を用意してないと思いますので、「Create S3 bucket」を選択します。(もし既に作成済みなら該当するbucket名を選んでください)
f:id:ysmn_deus:20200214134603p:plain
適当なbucket名を入力します。
f:id:ysmn_deus:20200214140249p:plain
(email-ドメイン名、など分かりやすい名称がいいとは思います。)

prefixなど設定できますがたぶんS3のパフォーマンスを落とすだけなので無視しましょう。
「Next Step」をクリックします。

ルールの詳細設定(名前決めるだけ)

「Rule Details」の項目が出てきます。
基本的に「Rule name」の箇所だけ適当に入力して、「Next Step」で問題無いかと思います。

最終確認

今まで設定してきた項目が一覧で確認できます。
何か間違いがあればここで修正できます。大丈夫であれば右下の「Create Rule」でルールを作成します。
f:id:ysmn_deus:20200214140952p:plain

問題無ければSESのコンソール上で表示されている筈です。
f:id:ysmn_deus:20200214165126p:plain

Step 4: メール送信テスト

まだ設定半ばなのですが、一端ここまでの設定が正しいか確かめてみます。
ドメイン名の前は何でも良いので、「test@設定したドメイン」のように適当なアドレスに向けてメールを送ってみます。
(例えば、example.comというドメインを設定したなら「test@example.com」のような。@の前は何でも良い)

Step 5: 受信したメールを見てみる

設定がうまくいっていれば、先ほどテストで送ったメールがS3に保存されている筈です。
S3のコンソール( https://console.aws.amazon.com/s3/ )から先ほど設定したbucketを見つけて中を見てみましょう。
f:id:ysmn_deus:20200214170821p:plain
bucketの中には - AMAZON_SES_SETUP NOTIFICATION - 謎の英数字の羅列 の2ファイルが存在していると思います。
謎の英数字の羅列が保存されたメールとなります。ダウンロードして中身を確認してみると、メールのソースになっていると思います。
(いわゆる本文だけではなく送信元情報などのデータとしてのメール。)

これでメールの受信が機能している所までは良さそうです。

Step 6: S3にPUTされたらLambdaで転送する様にする

S3にあがるだけではIMAPPOP3形式で受信できないので、どこかのメーラーでやりとりしたいものです。
(例えばGmailThunderbirdなど)
本来設定しているメールアドレスに一時的に転送して、閲覧は普段使いのメーラーでできるようにします。

転送用のLambda関数を作成する

何はともあれLambda関数が必要です。SAM CLIなどでデプロイしてもいいんですがそんなにいじらないし全部ブラウザ上から済ませます。

ローカルにaws-lambda-ses-forwarderをcloneする

まずローカルにGithubで公開されている aws-lambda-ses-forwarder をcloneします。

git clone https://github.com/arithmetric/aws-lambda-ses-forwarder

使い捨てるので適当なディレクトリで大丈夫だと思います。
f:id:ysmn_deus:20200214172950p:plain
必要なのはindex.jsのみです。(なんならコピペでもいいんですが・・・)

コンソールでLambda関数を作成する

ソースが準備できたらLambdaのコンソールに行きます。
https://console.aws.amazon.com/lambda/home
右上の「関数の作成」からLambda関数を作成します。
f:id:ysmn_deus:20200214173333p:plain

「一から作成」で基本的な情報を入力していきます。
関数名は適宜決めていただいて(例えば、「sesForwarder」など)、ランタイムは、現在(2020/02/14)ではNodeのv12が利用可能ですが、念のためv10にしておきました。(GithubのREADME上にはv8 v10と記載があるので、多分v12でも大丈夫だけど)
実行ロールは後で編集するとして、特に触らずに「関数の作成」でLambda関数を作成します。
f:id:ysmn_deus:20200214173938p:plain

とりあえずLambda関数はできました。
f:id:ysmn_deus:20200214174259p:plain

作成した関数をaws-lambda-ses-forwarderに書き換える

先ほどcloneしたaws-lambda-ses-forwarderの中にある「index.js」をアップロードします。
めんどくさい人はエディタで開いてコピーしてからブラウザ上のエディタに貼り付けても良いと思います。
とりあえずzipに固めてアップロードする手法を採ります。「index.js」のみzipに固めます。
f:id:ysmn_deus:20200214174436p:plain

固めたzipをLambdaにアップロードします。
先ほどのコンソールから「関数コード」という項目を探しだし、「コードエントリタイプ」から「zipファイルをアップロード」を選択します。
f:id:ysmn_deus:20200214174611p:plain

「アップロード」という箇所が出てくるので、先ほどのzipをアップロードする。
f:id:ysmn_deus:20200214174744p:plain

「アップロード」の横にzipファイル名が記載されていればアップロードできているので、「保存」をクリックしてアップロードしたzipファイルを関数に適応する。
f:id:ysmn_deus:20200214174934p:plain

Lambdaのコンソール上のエディタにaws-lambda-ses-forwarderが表示されていればOK
f:id:ysmn_deus:20200214175114p:plain

aws-lambda-ses-forwarderの設定を書き換える

これでアップロードは完了しましたが、ちょっとだけ設定を変更します。
30行目のdefaultConfigの値を変更していきます。

var defaultConfig = {
  fromEmail: "noreply@example.com", // ここは設定したドメインの任意のメールアドレス
  subjectPrefix: "[Forwarding]", // 転送するタイトルの頭に何か付けるならここに設定
  emailBucket: "s3-bucket-name", // emailが格納されるS3のbucket名
  emailKeyPrefix: "", // SESの設定でprefixを設定している場合は設定。上記の通りであれば空でOK
  forwardMapping: {
    "info@example.com": [
      "example.john@hoge.com",
      "example.jen@hoge.com"
    ],
    "@example.com": [
      "example.john@hoge.com"
    ]
  }
};

SESは仕様上、転送するメールアドレスの差出人を自分のドメイン以外にすることができません。
つまり、「hogefuga@hoge.com」からメールが来た場合に、「example.john@hoge.com」へメールを転送するのですが、上記の設定では、「example.john@hoge.com」へ届くメールの差出人は「noreply@example.com」に書き換わっています。

f:id:ysmn_deus:20200214182932p:plain
図示するとこんなかんじ

forwardMappingのみ追加で説明しますと、上に記載されているものから順にマッチするか評価され、最初にマッチした宛先に転送されるようです。
つまり上の例では - info@example.comはexample.john@hoge.comとexample.jen@hoge.comに転送される - info2@example.comはexample.john@hoge.comのみに転送される という処理になります。
基本的には1対1か1個のメールアドレスに集約されるように書くのがいいんじゃないでしょうか。

変更した後は右上の「保存」をクリックすることをお忘れ無きよう。

Lambda関数のロールの設定

現状ただのLambda関数ですのでSESはおろかS3にアクセスすらできません。
なので - S3へのアクセス権限 - SESでの送信権限 - ついでにCloudWatchのログ作成権限 を付与します。

コンソール上の下の方にある「実行ロール」という項目を確認します。
f:id:ysmn_deus:20200214184038p:plain

「既存のロール」という項目の下にある「【関数名】-role-(英数字の羅列)ロールを表示」という箇所をクリックします。
するとIAMのコンソールが表示されますので、「インラインポリシーの追加」を押します。
f:id:ysmn_deus:20200214184359p:plain

「ポリシーの作成」というページが表示されますので、編集していきます。
設定する対象がわかりきっている場合はJSONの方が早いです。「JSON」をクリックして下記のように設定しましょう。
f:id:ysmn_deus:20200214184556p:plain

{
   "Version": "2012-10-17",
   "Statement": [
      {
         "Effect": "Allow",
         "Action": [
            "logs:CreateLogGroup",
            "logs:CreateLogStream",
            "logs:PutLogEvents"
         ],
         "Resource": "arn:aws:logs:*:*:*"
      },
      {
         "Effect": "Allow",
         "Action": "ses:SendRawEmail",
         "Resource": "*"
      },
      {
         "Effect": "Allow",
         "Action": [
            "s3:GetObject",
            "s3:PutObject"
         ],
         "Resource": "arn:aws:s3:::S3-BUCKET-NAME/*"
      }
   ]
}

S3-BUCKET-NAMEの箇所はemailが保存されているS3のbucket名に変更して下さい。
厳密に言えばSESのリソースなども対象を絞った方がセキュアだと思いますが、その辺は割愛します。

JSONの編集が完了したら、右下の「ポリシーの確認」をクリックします。

最後に、設定したポリシーがどのようなものか一覧で表示されます。
ポリシー名を付けないといけないので「名前」の箇所を「LambdaSesForwarder」としておきました。
f:id:ysmn_deus:20200214185106p:plain

問題無ければ「ポリシーの作成」をクリックして完了します。
また別のLambdaに別のメールアドレスを設定する可能性などある場合はポリシーを作成しておくと再び作成しなくていいのですが、どうせS3のbucketも変わると思いますのでインラインポリシーで良いと思います。

Lambda関数のタイムアウト時間の変更

大丈夫な気がするんですが、一応aws-lambda-ses-forwarderのREADMEによると「メモリ制限が128MBで10秒がええんとちゃうか?」のような記述があるので、タイムアウト時間だけ10秒に変更しておきます。
(大きな添付ファイルが想定される場合はメモリを512MBにするか、タイムアウトを30秒にするかした方がいいかも、とも書かれているので心配な方は30秒に設定しておいた方がいいかも)

設定を変える場所は先ほど編集していた「実行ロール」の隣にあると思います。「基本設定」という項目を見つけ「編集」をクリックして下さい。
f:id:ysmn_deus:20200214185949p:plain

タイムアウトを10秒に変更して、「保存」をクリックします。
f:id:ysmn_deus:20200214190059p:plain

SESの設定にLambda関数を追加する

ここまできてようやくSESとLambdaを連携させます。
「保存先の設定(Actionの設定)」で編集していたSESのコンソールの「Email Receiving」→「Rule Sets」を編集します。
今までの通りにやっていれば「default-rule-set」が適応されていると思いますので、「View Active Rule Set」をクリックし、設定したRule(たぶん一番上に表示されてるヤツ)をクリックして編集します。
f:id:ysmn_deus:20200214191042p:plain
f:id:ysmn_deus:20200214191143p:plain

Add actionからLambdaを選択します。
f:id:ysmn_deus:20200214191317p:plain

対象の関数は作成した関数名を入力すれば出てくると思います。
それ以外は基本デフォルトで。
f:id:ysmn_deus:20200214191600p:plain

右下の「Save Rule」を押して編集したルールを保存します。
Missing Permissionsと聞かれますので、「Add permissions」で作成したSESにlambdaを起動する権限を付与します。

SESの設定で転送先のメールアドレスを認証しておく(一時的なもの)

おそらくスパム対策だと思いますが、初期状態ではSES+Lambdaでメールを送信する場合は転送先のメールアドレスを前もって認証しておく必要があります。
(後述する AWSのサポートへ連絡 でどのメールアドレスにも送信できるようになります。)
試験的にメールを送信するために、SESのコンソールから転送対象の認証を済ませます。
コンソールの「Identify Management」→「Email Addresses」をクリックし、「Verify a New Email Address」をクリックします。
f:id:ysmn_deus:20200214194422p:plain

Lambda関数のところで転送先を編集したと思うので、そのアドレスを入力します。
(上記の例では「example.john@hoge.com」と「example.jen@hoge.com」の二つ)
f:id:ysmn_deus:20200214194635p:plain

「Verify This Email Address」をクリックすると、前の画面に戻ってpendingになっているのが確認できます。
おそらく「Amazon Web Services – Email Address Verification Request...」というタイトルのメールが届いていると思いますので、記載されているリンクを24時間以内にクリックします。
「検証に成功しました」的な旨のページが出ればOKです。
f:id:ysmn_deus:20200214195036p:plain

コンソール上で先ほど設定したメールアドレスがpendingからverifiedに変わっていれば転送の準備は完了です。
f:id:ysmn_deus:20200214195239p:plain

転送されるか試しにメールを送ってみる

とりあえずメールを送りましょう。宛先は何でも良いと思いますが、Lambda関数の中でマッピングを複雑にした場合は色々試してみると良いと思います。

EX: S3のライフサイクルポリシー

上記のままだとメールが未来永劫たまり続けます。
基本残したままで困る容量ではないですが、SES+Lambdaで究極のコスパを攻めるならS3には極力データは残しておかない方がいいかもしれません。
S3にはライフサイクルポリシーという生成されてからどの期間残しておくかが設定できますので、そちらを設定しておけば極力容量は抑えられるとは思います。
ここでは割愛しますが、需要があればそのうち追記します。

メール送信のセットアップ

と項目を書いたのですが、受信のセットアップが終わっていればほぼ完了しているも同然です。
とはいえまだSMTPの情報などがないのでセットアップしていきましょう。

SMTPの情報を取得する

SESのコンソールから「Email Sending」→「SMTP Settings」をクリックします。
表示されている「Create My SMTP Credentials」をクリックします。
f:id:ysmn_deus:20200214231439p:plain

「Create User for SMTP」というページが出てくるので、とりあえずIAM User Nameはデフォルトで入ってる適当な名称を利用します。

f:id:ysmn_deus:20200214231952p:plain
特に変更しない

右下の「作成」でSMTPのユーザーを作成します。
f:id:ysmn_deus:20200214232107p:plain

書いてあるとおり、認証情報はこのタイミングでしかダウンロードできないので慎重に保存しておきます。
最悪ユーザーを作り直せば良いだけだと思いますが、くれぐれもこの辺の認証情報を公開しないように注意しましょう。
上記の画像に書いてある「ユーザーのSMTPセキュリティ認証情報を表示」でユーザー名とパスワードを表示して設定してもいいですが、ファイルに保存しておいた方が確実かと思いますので、右下の「認証情報のダウンロード」から認証情報を保存しておきましょう。

試験的に送信してみる

先ほどのSMTPの情報で、試しに認証している(上記までの例では、転送先のメールアドレス)にメールを送信してみて下さい。
おそらくSMTPのポートを587に設定して上記の情報を利用すれば送信できると思います。

Sandbox外への払い出し

SESのサービスを利用し始めたときは、認証したメールアドレスにしかメールを送ることができません。
(転送だけしかしない場合はこれでも問題ないでしょう)
なので、認証してないメールアドレスにも送信できるようにAWSに申請する必要があります。

AWSのサポートへ連絡

サポートに連絡するためにコンソール( https://console.aws.amazon.com/ )を開きます。
右上にある「サポート」から「サポートセンター」を開きます。
f:id:ysmn_deus:20200215091101p:plain

「Create case」をクリックし、「Service limit increase」を選択します。
f:id:ysmn_deus:20200215091414p:plain

すると、「Case classification」というボックスが出てくると思いますので、「Limit type」の「SES 送信制限」を選択します。
f:id:ysmn_deus:20200215095237p:plain
その他の入力項目は適宜合わせて貰えば良いと思いますが、今回は普段使いのメールなので「メールの種類」を「その他」、「ウェブサイトのURL」を空欄、残りの項目を「はい」にしました。
最後の3項目は日本語だとなんのこっちゃ・・・という感じですが、原文は

  • For My email sending complies with the AWS Service Terms and AUP, choose the option that applies to your use case.
  • For I only send to recipients who have specifically requested my mail, choose the option that applies to your use case.
  • For I have a process to handle bounces and complaints, choose the option that applies to your use case.

と記載されていますので

  • AWSサービス条件とAUP(ウェブサイトでリンクがありますのでご確認ください)を満たしているか
  • 受け取る意思がある人にのみメールを送信するか(メールのやりとりなので、よっぽど特殊なケースでない限りはあるはず。不特定多数の営業とかには使っちゃダメですね。)
  • 苦情などが来たときにハンドリングする手順があるか(普通のメールなので基本的には考えなくて良いと思います)

といった感じでしょう。アプリケーションなどにも活用を考えている場合はこのへんは慎重に回答してください。

次に、「Requests」の項目を埋めます。
リージョンを自分のSESのリージョンに合わせ、「Limit」を「希望する一日あたりの送信クォータ」に設定し、「New limit value」を「200」に設定します。この200は公式ドキュメントに書いてあります。 基本的にはドキュメント通りに設定すれば問題無いと思います。1日に200通以上も送る(受信ではなく送信)人は通常の人ではないので僕は知りません。
f:id:ysmn_deus:20200215100127p:plain

次に、「Case description」を記入します。
基本的にはありのままを記載すれば良いと思いますが、「Case classification」ではいと答えている項目には言及しておいた方がいいかもしれません。
自分は普通にメールとして使うよ!ということと、苦情対策としてはCloudWatchでSESの送信制限を監視して、ヤバそうだったら利用を停止するよ!ということを書いておきました。

最後に「Contact options」ですが、基本的にはデフォルトのままで良いと思います。「Preferred contact language」はもしかしたら英語の方がレスポンスが早いのかなぁとも思ったんですが、その後でやりとりが発生する可能性を考慮して一応日本語にしておきました。
「submit」を押して起票完了です。
f:id:ysmn_deus:20200215100956p:plain

たぶん1日ぐらいで返信が来ます。(自分は約1日で来ましたが、2~3日たっても気長に待ちましょう。)
「お客様のアカウントを Amazon SES サンドボックスから移動いたしました。」との記載があれば問題無く利用できる状態になっていると思います。