プログラマーのメモ書き

伊勢在住のプログラマーが気になることを気ままにメモったブログです

LINE WORKS で予約投稿ボットを作成する

訳あって、 LINE WORKS でボットを作ることになりました。今回試すのは、予約投稿を可能にするボットです。

予約投稿といっても、別段難しい話ではなく、

  • ユーザーが LINE WORKS 上で予約投稿ボットに投稿
  • 一定時間後に宛先ユーザーに予約投稿ボットから投稿される

という動作をするものです。いわゆる言ったことをそのまま返す、オウム返しボットの応用になります。

今回は予約投稿ができることを確認するためのサンプルなので、

  • 宛先ユーザーやボットからの送信日時を、ボットとの対話を通じて指定する機能
  • 予約投稿の確認やキャンセル

などは省略しています。投稿した内容を一定の固定時間おいて宛先ユーザー(これも固定)に配信するだけのサンプルになります。

このサンプルを作った顛末を参考までにメモっと来ます。

なお、元ネタとしては、下記のブログで紹介されている記事を参考にさせていただきました。ありがとうございました。

LINE WORKSで初めてのBot開発!(前編) - Qiita

LINE WORKSで初めてのBot開発!(後編) - Qiita

構成

今回作るものの構成はこんな感じです。

f:id:junichim:20190611172647p:plain

cacoo で描きました)

ユーザーが予約投稿ボットに投稿すると、同じ内容を別ユーザーに時間をおいて投稿します。

ボットが通信する送受信サーバーとして、 AWS Lambda を使います。予約投稿が終わるまで Lambda を実行中にするわけにはいかないので、 CloudWatch Events に登録し、指定時間に Lambda を呼び出すようにして、投稿処理を行いたいと思います。

とりあえず思いついたのが上記の構成なので、もっといい方法があるよ、という方、ぜひ教えてください。

準備

まずは、LINE WORKS のボット API を使うための準備をします。LINE WORKS をまだ使ってなければ、無料プランがあるので、申し込みましょう。

ボットAPIを使うための準備

LINE WORKS の申請をしたのが自分自身なら、最初から Developer の権限を持っていると思いますので、 LINE WORKS Developer Console にログインできると思います。

もし、会社や組織等で LINE WORKS を使っており、自分のアカウントに Developer の権限がない場合、Developer Console にログインできません。その場合は、管理者の方に相談して、ログインできる権限をもらってください。

LINE WORKS Developer Console にログインしたら、APIのメニューより、API ID / Server API Consumer Key を発行しておきます。

f:id:junichim:20190612103929p:plain

あと、LINE WORKS のAPIを使うためには、サーバートークンが必要になりますが、これは、サーバーの登録方法により発行方法が異なっています。

サーバーの登録方法は、固定IPとID登録の2種類があります。 今回はサーバーとして Lambda を使うので、固定IPタイプではなく、ID登録タイプを選択します。

ID登録タイプの場合、サーバーの登録(Server List への登録)は、先ほど発行した Server API Consumer Key を登録し、サーバーIDと認証キー(秘密鍵)を発行します。 サーバー名は好きなように指定します。これは自分がサーバーを区別するために使うだけで、認証等では使わないようです。

f:id:junichim:20190612104132p:plain

最終的にサーバートークンは、これらを用いて、JWT (Json Web Toekn) を利用し、専用APIを呼ぶことにより発行しますが、この部分はボットAPI利用時に Lambda 側で行う処理になります。

ということで、Developer Console ではサーバーIDと秘密鍵の発行までやっておけばOKです。 なお、このあたりの一連の手順は、下記公式ドキュメントに詳しく載っていますので、ぜひご覧ください。

API 認証の準備

ボットの登録

次に、LINE WORKS 上にボットを登録します。

LINE WORKS は1契約が1テナントという単位で管理されています(こちらの料金のページにある『Q.ユーザー/メンバーとライセンスの違いを教えてください。』にテナントについての説明があります)。

1テナントがさらに複数のドメインという単位に分かれているそうですが、通常は1テナント1ドメインなので、気にしなくていいようです(グループ企業全体で1テナント、子会社単位で複数ドメインというのがあるそうですが、特殊なケースだそうです。詳しくはこちらの『Botとドメインの関係につきまして』をご覧ください)。

LINE WORKS のボットは、 LINE のボットと異なり、LINE WORKS の管理者(テナントとドメインの管理者)がボットを登録すれば、そのドメインのユーザー全員が使えるようになります(正しくはボットを使うユーザーを管理者が指定でき、最大の範囲が全員になります)。

ということで、最初にテナントに対して登録し、次にドメインに登録します。

テナントに登録

テナントに対するボットの登録は、 LINE WORKS Developer Console でボットを登録すればOKです。 Botメニューを開き、『登録』ボタンを押すと、入力画面が表示されるので、ボット名など必要な項目を入力して、『保存』ボタンを押せば完了です。

f:id:junichim:20190611220805p:plain

なお、この時点では、 Callback URL (ボットがユーザーからの投稿も受け付けた際に呼び出すURL)がまだ未定ですので、Off のままにしておきます(あとから変更できます)。 また、『ボットポリシー』の『トークルームへ招待』は招待不可としておきます(各ユーザーと予約投稿ボットのやり取りになるので)。

ドメインに登録

次に、ボットをドメインに登録します。

LINE WORKS にログインし、管理者画面を開きます。上部のメニューより『サービス』を選択し、左側のメニューより『Bot』を選びます。

f:id:junichim:20190611221009p:plain

追加ボタンを押すと、Developer Console で登録したボットが表示されますので、選択して追加を行ってください。

テナント、ドメインや登録したボットの状態に関する説明は、下記公式ドキュメントをご覧ください。 LINE WORKS Developers

メッセージ受信処理

さて、ここまでくれば準備OKです。まずは、ボットからのメッセージ受信処理を作成します。

ユーザーからの投稿を LINE WORKS ボットが受け取り、その投稿(メッセージ)がボット登録時に指定した Callback URL に POST されます。 なので、受信メッセージサーバーを立てて、POST でメッセージを受け取るようにします。

これは具体的には API Gateway と Lambda で実装します。

Lambda 関数の作成

受信メッセージを処理する Lambda 関数を適当な名前で作成します(今回は reservePost としました)。 ランタイムは、 Node.js 10.x を選んでおきます。

実行ロールは、IAM を使って事前に作成したものを指定します。 例えば、今回は、 下記のように

f:id:junichim:20190611224026p:plain

  • AWSLambdaBasicExecutionRole
  • CloudWatchEventsFullAccess

というポリシーを持つロールを作成して、これを指定しました。

とりあえず、中身は未実装にしておき、API Gateway の設定を先にします。

API Gateway

API Gateway のほうは適当なリソース名を入力し、POSTメソッドを追加します。

f:id:junichim:20190611222830p:plain

統合タイプに Lambda 関数を指定し、『Lambda プロキシ統合』にチェックを入れ、Lambda関数を指定しておきます。

設定ができたら、API のデプロイを行います。デプロイができたら、URLが表示されますので、それを LINE WORKS Developer Console のボット登録を行った画面で編集を行い、 Callback URL として設定します。

LINE WORKS ボットからのメッセージを受け付ける Lambda 関数の実装

これで、LINE WORKS ボットがメッセージを受け取ると、 Callback URL ( API Gateway ) を通じて、Lambdaが呼ばれるようになりました。 この受信メッセージ用のLambdaでは、主に次の処理を行います。

  1. 受信メッセージのチェック(改ざん確認)
  2. 予約投稿したい内容の取得
  3. CloudWatch Events へ登録
受信メッセージのチェック

最初に行うのは受信メッセージのチェックです。 受け取ったメッセージが正しく LINE WORKS ボットから送られたものであるか(改ざんがないか)を確認します。

確認方法を公式ドキュメントで見ると、X-WORKS-Signature のヘッダを使うとの記述があります。

これについては、 API Gateway から Lambda に送られてくるパラメータにどういう形で設定されているか一度確認しておきます。 Lambdaの先頭に、

    console.log("event: " + JSON.stringify(event));

とログを埋め込んでおきます。

この状態で、 LINE WORKS にログインして、予約投稿ボットにメッセージを送ると、

f:id:junichim:20190611224955p:plain

下記のように Lambda のログ(CloudWatch Logs)に event の内容が出力されているのがわかります。

    "body": "{\"type\":\"message\",\"source\":{\"accountId\":\"LINE WORKS のユーザーID\"},\"createdTime\":1559921558817,\"content\":{\"type\":\"text\",\"text\":\"てst2\"}}",

また、headers に

    "headers": {
        ・・・
        "x-works-botno": "nnnnnn",
        "x-works-signature": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    },

と追加ヘッダが存在しているのがわかります。

使うべき情報されわかれば、あとは公式ドキュメントの通りに処理を実装します。

const crypto = require("crypto");

function isValidMessage(body, signiture) {
    const hmac = crypto.createHmac("sha256", API_ID);
    hmac.update(body);
    const str = hmac.digest('base64');

    console.log("message body: " + body);
    console.log("message hmac: " + str);
    console.log("signiture   : " + signiture);

    return str === signiture;
}

これで、メッセージの改ざんチェックができるようになりました。

(参考) [Sy] HMAC-SHA-256 を計算するサンプル(JavaScript) | Syntax Error.

予約投稿したい内容の取得

event.body は文字列なので、parseして、JSONオブジェクトに変換しておきます。

    const body = JSON.parse(body_str);

body オブジェクトに type プロパティがあります。これの値で送られてきたメッセージの種類が判別できます。

最初にボットとの会話を開始すると type が join / joined になったメッセージもやってきます。 ここでは、 type が message のもの(一般のメッセージ)を対象にします。message の場合は、

  • body.source.accountID に送信者のID
  • body.content.text に送られたメッセージの内容

などにほしい情報があるので、これらを取得しておきます。詳しくは公式ドキュメントをご覧ください。

なお、今回は、予約投稿を実施する日時、送信先は固定で扱ってるのでここでは取得していません。

CloudWatch Events への登録

受信処理の最後は、指定された日時に送信用メッセージサーバ(Lambda関数)を呼び出すように、CloudWatch Events を構成します。

CloudWatch Events をAPI で生成するには、

  • putRule を呼び出して、ルールを作成
  • putTargets を呼び出して、ルールに対してターゲット(Lambda関数)を指定

という流れになります。

ルールの作成

ルールは、指定時間に呼び出したいので、cron式で指定します。 例えば、現在時刻の3分後に投稿するならば、

function getCronExpression() {
    // cron 式の生成
    // UTC
    const nw = new Date(Date.now() + 3 * 60 * 1000); // 3分後

    const SP = " ";
    const minute = nw.getUTCMinutes();
    const hour = nw.getUTCHours();
    const date = nw.getUTCDate();
    const month = nw.getUTCMonth() + 1;
    const year = nw.getUTCFullYear();
    return "cron(" + minute + SP + hour + SP + date + SP + month + SP + "?" + SP + year + ")";
}

のようにcron式を作ればOKです。なお、cron式の日時指定はUTCになります。

このcron式を使って、putRule呼び出し用のパラメータを構成します。

async function createCloudWatchRule() {
    let cwe = new AWS.CloudWatchEvents();
    let params = {
        Name: 'post_reservation', /* required */
        Description: 'LINE WORKS post reservation, from, to, at,',
        ScheduleExpression: getCronExpression(),
        State: "ENABLED"
    };

Name はルールの名前になります。ルール削除時にも使います。

また、同じルール名を指定して putRule を呼ぶと、エラーになるのではなく、既存のルールが更新されます。このため、複数の予約投稿を扱いたい場合はルール名が一意になるように工夫する必要があります(今回は未対応です)。

ScheduleExpression にcron式を指定します。

パラメータが設定できれば、あとはputRuleを呼び出すだけです。

    return new Promise((resolve, reject) => {
        cwe.putRule(params, function(err, data) {
            if (err) {
                console.error("createCloudWatchRule err: " + JSON.stringify(err), err.stack);
                reject(err);
            } else {
                console.log("createCloudWatchRule: " + JSON.stringify(data));
                resolve(params.Name); // ルール名を返す
            }
        });
    });
}
ターゲットの指定

ターゲットの指定も同様に行います。

まず、ターゲット指定用のパラメータを作成します。

Arn に呼び出したい Lambda 関数のARNを指定します。Id にこのターゲットを特定するIDを指定します。putRuleと同様に同じIDを指定して putTarget を呼ぶと、設定内容が更新されます。また、IDを変えれば複数の Lambda 関数呼び出しを登録することもできます(同じ Lambda関数でもOK)。

async function assignRuleToLambda(rule_name, id_from, id_to, message) {
    let cwe = new AWS.CloudWatchEvents();
    let params = {
        Rule: rule_name,
        Targets: [
          {
            Arn: LAMBDA_ARN_SEND,
            Id: '1', /* required */
            Input: "{ \"rule_name\": \"" + rule_name + "\", \"id_from\": \"" + id_from + "\", \"id_to\": \"" + id_to + "\", \"message\": \"" + message + "\" }",
          }
        ]
    };

また、今回はサンプル実装のため、投稿する内容、宛先などの情報を Lambda 関数に対するパラメータとして渡しています。 これは、 Input 要素に JSON 文字列として記述することで渡すことができます。 パラメータの受け渡しには、 Input, InputPath, InputTransformer の3種類が使えます。詳しくはAWSのドキュメントをご覧ください。

あと、後述するように投稿完了後 CloudWatch Events のルールを削除する必要があるので、このルール名もパラメータとして渡しておきます。

パラメータができれば、 putTargets を呼び出せばOKです。

    return new Promise((resolve, reject) => {
        cwe.putTargets(params, function(err, data) {
            if (err) {
                console.error("assignRuleToLambda error: " + JSON.stringify(err), err.stack);
                reject(err);
            } else {
                console.log("assignRuleToLambda: " + JSON.stringify(data));
                if (data.FailedEntryCount != 0) {
                    console.error("assignRuleToLambda has error: " + JSON.stringify(data));
                    reject(data);
                }
                resolve(data);
            }
          });    
    });      
}

putTarets の戻り値には FailedEntryCount があり、これが1以上になっているとどれかのターゲット登録(または更新)に失敗したことを意味します。 上記では、FailedEntryCount が0であることを確認しています。

(参考)

CloudWatch から呼び出され、 LINE WORKS ボットへ投稿を行う Lambda 関数の実装

次に、CloudWatchEvents から呼び出され、LINE WORKS ボットへ投稿を行う Lambda 関数を実装していきます。 この送信メッセージ用のLambdaでは、主に次の処理を行います。

  1. パラメータの取得
  2. サーバートークンの取得
  3. LINE WORKS ボットへ送信
  4. CloudWatch Event Rule の削除

パラメータの取得

CloudWatchEvents のターゲット登録の際に Input 要素で指定していたパラメータを取得します。

呼び出された Lambda 関数の event オブジェクトがそのままInputで指定した JSON になっています。

    // パラメータ取得
    const id_from = event.id_from;
    const id_to = event.id_to;
    const org_msg = event.message;
    const rule_name = event.rule_name;

サーバートークンの取得

LINE WORKS Developer の公式ドキュメントにのっとって処理します。

まず、JWTを取得します。

const jwt = require('jsonwebtoken');

function getJwt() {
    const iat = Math.floor(Date.now() / 1000); // msec-> sec
    const exp = iat + (60 * 30); // 30分後
    let token = jwt.sign({
        iss: SERVER_ID,
        iat: iat,
        exp: exp
    }, PRIVATE_KEY, {algorithm: 'RS256'});
    return token;
}

次に、サーバートークン取得用のAPIを呼び出します。

const request = require("request-promise");

async function getServerToken() {
    const jwtoken = getJwt();
    console.log("jwtoken: " + jwtoken);

    const headers = {
        "Content-type": "application/x-www-form-uelencoded; charset=UTF-8"
    };
    const options = {
        url : TOKEN_URL,
        method : "POST",
        headers : headers,
        form : {
            "grant_type": encodeURIComponent("urn:ietf:params:oauth:grant-type:jwt-bearer"),
            "assertion" : jwtoken
        },
        json : true
    };

    return request(options)
        .then((body) => {
            console.log("getServerToken:" + JSON.stringify(body));
            return body.access_token;
        });
}

問題がなければ、サーバートークンが取得できます。

(参考)

LINE WORKS ボットへ送信

サーバートークンさえ取得できれば、あとは LINE WORKS ボットAPIを呼び出すだけです。

ヘッダにServer API Consumer Key と取得したサーバートークンを指定します。

async function sendMessage(serverToken, dst_id, org_msg) {
    const headers = {
        "Content-type": "application/json; charset=UTF-8",
        "consumerKey": SERVER_CONSUMER_KEY,
        "Authorization": "Bearer " + serverToken
    };

投稿する内容、ヘッダ、ボットNoを指定したオブジェクトを作成し、

    const options = {
        url : SEND_URL,
        method : "POST",
        headers : headers,
        json : {
            "botNo": Number(BOT_NO),
            "accountId": dst_id,
            "content": {
                "type": "text",
                "text": org_msg
            }
        }
    };

request-promise モジュールを使って送信用のボットAPIを呼び出せばOKです。

    return request(options)
        .then((body) => {
            if (body.code != 200) {
                console.error("sendMessage error: " + JSON.stringify(body));
                throw new Error(body.errorMessage);
            }
            console.log("sendMessage success: " + JSON.stringify(body));
            return "finish";
        });

}

CloudWatch Events Rule の削除

送信が無事に終わったら、 CloudWatch Events のルールを削除しておきます。ルールの削除は

  1. ルールに含まれるターゲットをすべて削除
  2. ルールの削除

の順番で行う必要があります。実際には、ルールに含まれるすべてのターゲットを指定するためにターゲットのリストアップも必要になるため、それぞれ

  1. ルールに含まれるすべてのターゲットIDを取得: CloudWatchEvents#listTargetsByRule
  2. ルールに含まれるターゲットをすべて削除: CloudWatchEvents#removeTargets
  3. ルールの削除: CloudWatchEvents#deleteRule

のAPIを呼び出して実行すればOKです。ここにはコードは載せませんが、気になる方は GitHub リポジトリのほうを見てください。

実行

これで一通りの実装が終わりました。テストしてみます。

LINE WORKS にログインして、一人のユーザーから

f:id:junichim:20190612092638p:plain

『ブログ用にテスト』という内容で予約送信ボットに投稿を投げました。

するとおおよそ3分後、別のユーザーに対して

f:id:junichim:20190612093841p:plain

のように投稿が送られてきました。

投稿が行われる前に、AWS にログインして CLoudWatch Events を見ると、ルールが

f:id:junichim:20190612101345p:plain

のように設定されており、ターゲットも、

f:id:junichim:20190612101456p:plain

のように追加されていました。もちろん、投稿が終わると削除されました。

うまく動作しているようです。

今後

さて、ここまでで予約投稿を行うことはできたのですが、最初に書いたように実際に使えるようにするためには、ボットとの対話が欲しいところです。

今回の予約投稿だと、

  • 宛先
  • 送信日時
  • 送りたい内容

の3項目を入力する必要があります。これだけなら、自然言語処理とか入れずに自分で実装することもできるかな?と一瞬思ったのですが、やはりそれなりに大変そうなのでやめました。

そういうわけで、ボット用のフレームワークを探しているところです。 最近、AWS ばっかり使って Amazon にベンダーロックインされている身としては、Amazon Lex を使いたいところなのですが、Lex は日本語に対応していないとのこと。 なので、この先どう進めるか思案中です。

今のところ DialogFlow が有力かな?そのあたりは実装したらまた記事にまとめたいと思います。

あ、ソースコード一式を GitHub に置きました。

github.com

もしご興味があれば試してみてください。

補足

実装時の注意点を最後にいくつか補足しておきます。

Lambda の環境変数でのプライベートキーの指定方法について

今回実装した Lambda 関数では、サーバートークンを取得するための秘密鍵を環境変数で与えています。 秘密鍵を環境変数で与える際には、改行文字も正しく与える必要があるようです。

なので、下記記事などを参考に、

[RS256] JWTでRSA秘密鍵を環境変数で処理したい [Javascript] - Qiita

$ cat private.key | sed -e :xxx -e 'N; $! b xxx' -e 's/\n/\\n/g'

(xxxはラベル名なのでなんでもOKです)

として、1行化し、Lambda のコンソールから環境変数に設定します(コンソールで入力する際はダブルクォーテーションで囲む必要ありません)。

Lambda 側では

const PRIVATE_KEY = process.env.LINE_WORKS_PRIVATE_KEY.replace(/\\n/g, "\n");

のようにして、1行化した文字列を複数行に直しています。

(参考) sed のラベルコマンドなどについて詳しく解説してくれています。

sed ファイル内の改行を削除, 連続した空白を1つにする - Qiita

Lambda 関数のローカル開発について

こちらの記事にあるように Lambda の開発では index.js を呼び出す proxy.js などを作成し、そちらで dotenv を使って環境変数を定義すると、Lambda にアップロードするコードを変更することなく、ローカルでも

$ node proxy.js

のように実行できる、という形が作れます。

API Gateway からの呼び出しを模擬したければ proxy.js 内で定義している event に API Gateway から送られてくる event を張り付けてやればよいかと思います(eventの内容は一度 API Gateway 経由で Lambda を動かし、 CloudWatch Logs で確認すればよいかと思います)。

とはいえ、毎回、API Gateway からの呼び出しを模擬するのも面倒だったので、途中からはローカルの開発環境ではパッケージのインストールぐらいしか行ってなかったんですが、手元でさっと動かせるのは便利でした。

ご参考までに。

Twilio からのメールを Amazon SES 経由で送信する

以前、Twilio を使って電話/FAXを受けたら、メールを飛ばすというのをやりました。

Twilio 電話を受けたら録音して、メールを飛ばす - プログラマーのメモ書き

Twilio 試した(3/3):FAX 受信と通知を試しました - プログラマーのメモ書き

このとき、メール配信に利用していたSMTPサーバーが、さくらインターネットのサーバーだったんですが、上記記事にあるように、さくらインターネットの場合、『国外IPアドレスフィルタ』を解除してやる必要がありました。

この運用形態はどうにも心配な状態です。

というのも、この『国外IPアドレスフィルタ』の制限を解除すると SMTP だけでなく、いろんなサービスへのアクセスが制限されなくなります。また、さくらインターネットの場合、メールの送信先制限などがありません。つまり、

  • 国外IPアドレスフィルタ制限がプロトコル単位で切り替えできない
  • 国外IPアドレスフィルタ制限を有効にした場合のホワイトリスト設定は http のみ可能であり、SMTPについてはホワイトリスト設定ができない
  • 国外IPアドレスフィルタ制限を解除して、スパムメールの踏み台になった場合への保険として、送信先制限(ドメイン名やメールアドレスによる制限)をやろうとしたができなかった

という状態になっています(さくらインターネットさんをディスってるわけではないですよ。国外IPアドレスフィルタを使いたいだけです)。

ということで、このまま使い続けるのは不安だなと感じていた次第です。

で、今回いろいろと考えて得た結論としては、

  • さくらインターネットの SMTP サーバーではなく、別の SMTP サーバーを利用する
  • SMTP サーバーをメールサーバーのレンタル(メールアドレスの申し込み)ような形で借りると費用も掛かるので、送っただけ課金されるようなものを探す

となりました。

これに該当するものを具体的に調べると、Amazon SES (Simple Email Service) を利用すればうまくできそうだったので、Twilio発SES経由でのメール配信に切り替えました。今回はそのメモ書きになります。

SES の設定

詳しくは SES の紹介ページを読んでいただくほうがいいのですが、メール送受信サービスになります。SES には、SMTPインターフェースもあるので、これを使ってメールを送ることにします。

下記ページに詳しく書かれていますが、SES を使う際には最初に送信者のID(メールアドレスまたはドメイン)を検証する必要があります。

Amazon SES の ID の検証 - Amazon Simple Email Service

また、サンドボックスモードという状態(初期状態がこれ)では、この検証されたメールアドレス宛にしか送信できません。 今回は自分あて(自分のドメイン宛て)のメールを扱うことにするので、ドメイン認証を行い、サンドボックスのまま利用することにします。

いろいろ書くと大変そうですが、実際に試してみるとわかるように、SES を使い始めるのは非常に簡単です。

ドメイン認証

今回はドメイン認証を行います。

Amazon SES のドメイン検証 TXT レコード - Amazon Simple Email Service

最初にAWSのコンソールにログインして、SESの画面を開きます。

SESのリージョンは、このブログを書いている時点で、

  • 米国東部(バージニア北部)
  • 米国西部(オレゴン)
  • EU(アイルランド)

のみのようです。これから適当なリージョンを選んでおきます。

メニューより、『Identify Management』の『Domains』を選択し、画面上部の『Verify a New Domain』ボタンを押します。

f:id:junichim:20190604142051p:plain

上記のような画面が表示されるので、検証したいドメイン名を入力します。今回は、自分のドメイン宛てにしかメールを送らないので、『Generate DKIM Settings』はチェックしません。 『Verify This Domain』ボタンを押すと、

f:id:junichim:20190604143826p:plain

のように、DNSに追加するべき、TXTレコードが表示されますので、これをDNSに追加しておきます。

しばらくすると、SESの画面で、

f:id:junichim:20190604144027p:plain

のように、『verified』と表示されればOKです。

テスト

この時点で一度メール送信のテストを行っておきます。

『Send a Test Email』ボタンを押すと

f:id:junichim:20190604144236p:plain

のような画面が表示されます。ここで、FromとToに同じメールアドレス(先ほど登録したドメインの有効なメールアドレス)を入力します。その他にもSubjectや本文などを入力後、『Send Test Mail』ボタンを押します。

指定したメールアドレスにメールが無事に届いていればテスト成功です。

バウンスと苦情メール

次に、メールが届かなかった場合の対応処理を指定しておきます。 デフォルトの状態だと、送信者(細かくは利用インターフェースおよび設定で異なります)にメールが返ってきます。

Amazon SES E メールでの通知 - Amazon Simple Email Service

これをメールではなく、SNS(ソーシャルネットワークではなく、 Amazon Simple Notification Service のほうです)に通知するように変えておきます。もっとも、SNSも指定したアドレスにメールを通知するだけなんですけどね。 もし、SNSのトピックがない場合は、先に次の節のSNSの設定をやっといてください。

Amazon SES 通知を使用したモニタリング - Amazon Simple Email Service

SESのコンソール画面のDomainsを開いて、設定したいドメイン名をクリックします。Notifications のところを開くと

f:id:junichim:20190604150413p:plain

のような画面が表示されるので、『Edit Configuration』をクリックして設定画面を開きます。

f:id:junichim:20190604150539p:plain

ここで、『SNS Topic Configuration』の欄で、 Bounces, Complaints のドロップダウンリストから、バウンスや苦情発生時に通知を行いたいSNSトピック名を選択します。

また、SNSを設定したので、通知を2重に受け取らないようにするため、画面下部の『Email Feedback Forwarding』もdisableにしておきます。

SNS (Simple notification Service) の設定

CloudWatch とかとは異なり、SES から SNS のトピックは作れないようなので、AWS のコンソールで SNS を開いて、トピックを作成します。 トピックの作成時にサブスクリプションとして、メールアドレスを指定します。

指定したメールアドレスに確認メールが送られるので、メール内のリンクをクリックすればOKです。

バウンスと苦情のテスト

ここで、もう一度バウンスと苦情メールが発生したときのテストをしておきます。

バウンスと苦情のテストは、実際に存在しないメールアドレスに送るとかではなく、先ほどのテストメール送信画面から特定のメールアドレスに送ることでシミュレーションされます。 詳しくは、下記のリンク先に書かれています。

Amazon SES での E メール送信のテスト - Amazon Simple Email Service

最初に、バウンステスト用のメールアドレスを使って、バウンスメールを発生させると、SNS のトピックで指定したメールアドレスに問題なく配信されていることが確認できました。苦情の場合も同じようにして確認しておきます。

SMTP インターフェース設定

あとは、SMTPインターフェースを設定すればOKです。

SES のコンソール画面より、『SMTP Settings』を選択すると、下記のような画面が表示されます。

f:id:junichim:20190604151527p:plain

SMTP サーバー名など設定で必要なものが表示されているので控えておきます。

次に、『Create My SMTP Credentials』をクリックすると、

f:id:junichim:20190604151815p:plain

のような SMTP 認証に用いる IAM ユーザーの作成画面が表示されます。 ここで、IAM ユーザー名を入力すると、

f:id:junichim:20190604151934p:plain

のような画面になるので、このセキュリティ認証情報を控えておきます。これが、SMTPユーザー名とパスワードに該当します。

Twilio 側の設定

さて、Twilio 側ではSMTPサーバー名および認証に用いるユーザー名・パスワードをSESのSMTPインターフェース用のものに代えてます。

これで、送信できるかと思い、Twilio のコンソールを開き、電話受信時に Function を表示した状態で、管理している電話番号に電話をしてみたところ、画面に表示されるログにエラーと出ています。

SES のドキュメント を見ると、 STARTTLS を使えば良さそうです。 そのためには、 nodemailer の設定を調べてみると、 secure と requireTLS を変更する必要がありそうです。

そこで、Twilio の Function 内の下記の部分を

        var transporter = nodemailer.createTransport({
            host: 'SMTP サーバー名',
            port: 587,
            secure: true,
            auth: {
                user: 'ユーザー名',
                pass: 'パスワード'
            },
            tls: {
                rejectUnauthorized: false
            }
        });

このように

        var transporter = nodemailer.createTransport({
            host: 'SMTP サーバー名',
            port: 587,
            secure: false,
            requireTLS : true,
            auth: {
                user: 'ユーザー名',
                pass: 'パスワード'
            },
            tls: {
                rejectUnauthorized: false
            }
        });

変更しました。

これで、再度試すと、問題なく録音データをメールで受け取ることができました。

さくらインターネットの国外IPアドレスフィルタ

ここまでくれば、最後は、さくらインターネット側の『国外IPアドレスフィルタ』を再度有効にしておけば終わりです。

これで、ちょっと安心できました。

備考

SPF と DKIM について

あと、SESの関連する設定について整理しておきます。

こちらの記事にあるように、デフォルトの設定で SES を使う分には SPF 設定は不要のようです。

一方、DKIM については設定するほうが望ましいのかもしれませんが、今回の場合は送受信者とも自分の管理するドメインのユーザーになるため、DKIMの設定は行いませんでした。 自分の管理するドメイン以外にメールを送信したい場合は、DKIMをセットアップしたほうがいいでしょうね、きっと。

サンドボックス解除後の送信先制限

今回は関係ないのですが、サンドボックスを解除したうえで、送信先制限をかけることもできるようです。 興味のある方は、下記のリンク先などを参考にしてください。

Amazon SESで送信元と宛先の制限をかけてみたメモ - Qiita

参考

下記記事等を参考にさせていただきました。ありがとうございます。

Dell T105 の電源交換

長らく使っているサーバー Dell T105 ですが、先日もこちらの記事でも書いたように、このサーバー上で オフラインマップの自動生成を乗せたり、遊ぶには十分使えております。

そんなある日、仕事場に来てみると、サーバーが止まってました。 ちょうど前の日にエアコンの工事をしていたので、その影響かな?と思って、電源ボタンを入れてみたのですが、反応がありません。

あれ?と思い調べてみたところ、電源ユニットがやられていたようです。

ということで、今の時代どれだけの需要があるかわかりませんが、電源ユニットを交換したので、その顛末をメモっと来ます。

トラブったときの状態

一応、最初にトラブったときの状態をまとめときます。

  • ある日、サーバーの電源が落ちていた
  • 電源ボタンを押しても、立ち上がらない
  • コンセントを抜いて、再度差し直すと、電源ボタンがオレンジで点滅してる
  • ハードウェアマニュアルを見ると、電源ユニットの不具合とのこと

点滅のところ、写真だとこんな感じでした(本当は点滅してます)。

f:id:junichim:20190531145207j:plain

確認

電源交換に先立ち、部品番号などを調べておきます。 コンセントを抜いて、カバーを開けて電源ユニットを確認すると、

f:id:junichim:20190531145625j:plain

この筐体の場合、 L305P-01 という部品番号のようでした。

交換部品の手配

ずいぶん昔のサーバー(調べると2009年10月頃に買ってました)なんで、交換部品あるだろうか?と思い、ネットを調べてみると、わんさかありました。 いや、今の時代すごいですね。それともサーバーだからなのかな?いずれにしても、助かります。

結局、毎度おなじみのamazonで、下記のリンク先から購入しました。

中国の業者さんのようで、到着まで10日以上かかるとのことでしたが、半分程度で着きました。ありがたいですね。

開封

早速、やってきた箱を開けると、

f:id:junichim:20190531150733j:plain

厳重というか、ぐるぐる巻きです。

背面側(電源ケーブル差込口側)

f:id:junichim:20190531150916j:plain

側面

f:id:junichim:20190531151000j:plain

amazonの商品紹介の写真だと、このケーブルクリップ(青い部品)がついていなかったので、心配でしたが、きちんとついていました。

ケーブル側

f:id:junichim:20190531151101j:plain

見た感じ、特に問題ないようです。なので、交換とまいまります。

ちなみに、新しい電源の型番は、 L305P-03 でした。

f:id:junichim:20190531155412j:plain

交換

いよいよ交換です。まあ、わざわざブログに書くほどでもないぐらい、交換作業は難しくありません。

取り外し

まずはコンセントを抜いて、シャーシを開けて、電源ケーブルを一通り外します。

f:id:junichim:20190531150646j:plain

次に背面側(外側)のネジ4本を外します。

最後にハードウェアマニュアルにもあるように、シャーシ側の『電源ユニットリリースタブ』を押して、電源ユニットを外します。

f:id:junichim:20190531151553p:plain

電源ユニット外した状態で写真撮り忘れたので、ちょっとわかりにくいですが、シャーシを開けた側からみると、底面側になる壁面に少し飛び出しているタブ状のものです。 これを押しながら、電源ユニットをずらすと簡単に外せます。

ちなみに、ハードウェアマニュアルには、ヒートシンクとエアフローカバーアセンブリも外せと書いてありましたが、私の場合これらをつけたままでも問題なく作業できました。

取り付け

次は取り付けです。 取り外しと逆の手順とすればOKです。

一点、ちょっと気を付けたいのが、電源ユニットのシャーシ側の側面には、

f:id:junichim:20190531154332j:plain

のような切り欠きがあります。で、取り付ける際には、この切り欠きに、シャーシ側のツメが引っかかるようにする必要があります。

そこさえクリアできれば、あとはネジ締めて、電源ケーブル取り付ければOKです。

あ、フラットケーブルはケーブルクリップに通して固定してください。ケーブルクリップ自体が結構曲がるのでわりと簡単に通せました(ですが、力の入れすぎにはご注意ください)。

動作確認

交換ができれば、最後に動作確認します。

一度はずした電源ケーブルなどをすべて取り付けて、電源ボタンを押すと、何事もなかったかのようにサーバーに電源が入りました。 しばらくすると、OSも問題なく立ち上がり、タイルサーバーも問題ないようです。

やれやれ。

おまけ

ケーブルクリップですが、ハードウェアマニュアルでは、交換部品にケーブルクリップがついていないことを想定しているようで、既存ものを取り外して使いまわせ、と書いてあります。

f:id:junichim:20190531154702j:plain

取り外した電源はあとで処分するだけなんで、思い切って外してみます。 すると、思ったより力が必要で部品が割れるかと思ったぐらいです。

f:id:junichim:20190531154800j:plain

外すとこんな感じでした。

もし、交換の電源を買ったのに、ケーブルクリップがなければ、頑張って付け替えてください。

参考記事

先人たちの記録が参考になりました。ありがとうございました。

サーバの電源が逝った(;´Д`) | でびあんのがらくた箱

【データ復旧実績】サーバー DELL PowerEdge T105 サーバーの電源が入らない – データ復旧専門【データテック】札幌駅1分 北海道データ復旧センター 無料診断・即復旧~

Dell PowerEdge T105 の電源ユニットが壊れたので取り替えた話 | Hello World!!