Slackのリマインダー機能まとめ

こんにちは、家でも会社でも実家でもSlackばかり使っているwakです。会社でSlackのリマインダー機能について聞かれたのでまとめておきます。元ネタは公式です。

f:id:nurenezumi:20160205123231j:plain

とにかく /remind

リマインダーはSlackの標準機能のひとつです。チャット欄(どこのチャンネルでもいいです)に/remindから始まるコマンドを入力すると、Slackが指定した日時にメッセージを送ってきてくれる便利機能です。忘れっぽいあなたには必携と言えるでしょう。また、通知先として自分以外にも他の人を指定できたりします(後述します)。

日時の指定方法にはいくつかのバリエーションがありますが、どの場合でもその場で通知スケジュールが表示されます。

f:id:nurenezumi:20160204162035p:plain

間違えていたら「Cancel」をクリックすればキャンセルできます。

1回だけの通知

1回だけ通知を送る場合、通知先には自分自身、他ユーザー(@yamada@tanakaなど)、パブリックチャンネル(#generalなど)が指定できます。プライベートチャンネルは指定できません。

○○分後、○時間後に通知してほしい

Slackは指定した時間だけ待ってから通知を送ります。

30分後、自分宛に「出かける時間だよ」と通知してほしいとき。

/remind me 出かける準備しろ in 30 min

1時間後、自分宛に「電話かけないと」と通知してほしいとき。

/remind me 早く電話かけろ in 1 hour

2時間後、@yamadaさんに「お腹空いたしそろそろお昼にしません?」と通知してほしいとき。

/remind @yamada ごはんですよ in 2 hour

3時間後、#generalに「経費申請を早くしろ」と通知してほしいとき。

/remind #general 経費! 申請! 経費! 申請! in 3 hour
  • 他ユーザー・パブリックチャンネルを送信先に指定した場合、設定を行った段階で即座に通知が行きます。他人をビックリさせる目的では使えません。
  • 英語の授業ではないので単数・複数形は気にしません。minminuteと書いてもminutesと書いてもいいです。
  • 語順に関しては柔軟です。次のどちらの書式でも受け付けてくれますが、混乱すると面倒なので固定して覚えておいた方がいいです。
/remind me 出かける準備 in 30 min
/remind me in 30 min 出かける準備

指定した時刻に通知してほしい

日付や時刻を絶対指定することもできます。こちらの方がよく使うかもしれません。

今日の23:30に「日付が変わる前にやることがあるよね?」と通知してほしいとき。

/remind me デイリーミッション全部終わった? on 23:30

もちろんその時刻を過ぎていれば明日になります。明日の0:25に何かテレビを見ることを通知してほしいとき。

/remind me てれび on 0:25

日付を指定することもできます。2/10 10:00に#project-neco宛に会議のお知らせを通知してほしいとき。

/remind #project-neco 今日はこれから会議ですよ on 2016-2-10 10:00

12時間制が好きな人はam/pmも指定できるようです。2/15 13:30にチョコレートのセールに行くように通知してほしいとき。

/remind me 売れ残ったチョコを回収しにいこう on 2016-2-15 pm 1:30

時刻については難しいことは考えず、日本時間でそのまま指定してください。またonは省略可能です。

繰り返し通知

「毎日」「毎週○曜日」「毎月○日」「毎年○月×日」の形で繰り返し予定を通知することもできます。こちらは他ユーザーを通知先に指定することはできません。自分自身、パブリックチャンネルのみが送信先にできます。

毎日通知してほしい

毎日15:00にお花に水やりをするように通知してほしいとき。

/remind me お花 on 15:00 every day

毎週○曜日に通知してほしい

毎週月曜日の9:30に自分宛に「先週分の経費を社内システムに登録した?」と通知してほしいとき。

/remind me 経費入れないと on 9:30 every monday

毎月○日に通知してほしい

毎月20日の9時半に請求書を送るように営業各位に通知してほしいとき。

/remind #dept-sales 請求書送るよ~ on 9:30 20th of every month

日付部分には3thのように恥ずかしい英語を書いても通りますが、「20 of every month」ではダメです(強制的に月初になります)。

毎年○月×日に通知してほしい

毎年5月30日の23時半に「もうすぐ妻の誕生日だよ」と通知してほしいとき。

/remind me ハッピーバースデーおーくさーん on 23:30 every 5-30

一覧表示

/remind list

で今設定されているリマインダーを一覧表示することができます。必要のないものは消しておきましょう。

おわりに

以上です。忘れっぽいのはなかなか直りませんが、テクノロジーの力で補完することはできます。使えるものは何でも使って幸せになりましょう。

Twilioを使ってWeb API経由で電話をかける(前編)

こんにちは、AWS担当のwakです。

f:id:nurenezumi:20160127154326j:plain

【インデックス(予定)】

  1. CloudWatchの監視結果をSlackに流す(AWS Lambdaバージョン)
  2. AWS LambdaでWebサイトの死活監視を行ってSlackに結果を流す
  3. DynamoDBにパラメーターを入れてハードコードをなくす
  4. CloudWatchの監視結果をTwilioに流して電話をかける(前編)(今回)
  5. CloudWatchの監視結果をTwilioに流して電話をかける(後編)
  6. SlackからEC2インスタンスを起動・停止するコマンドを作る

APIで電話をかけよう

Twilioというものがあります。これは電話に関する諸々をWeb APIでコントロールできるサービスで、電話を受ける・かける、録音する、SMSを送信するといった面倒そうな処理の面倒をまとめて見てくれる優れものです。

今回はこのサービスの機能のうち、

  • 電話をかける
    • 指定した番号に電話をかけることができます。
  • 日本語読み上げ
    • 指定した文章を、電話口で(そこそこ自然な)日本語で読み上げてくれます。
  • 音声ファイル再生
    • 読み上げの前後に、予め準備した音声ファイル(mp3など)を再生してくれます。

を使って、《CloudWatchでアラートが発生した際、電話でアラートを通知する》仕組みをサーバーなしで構築することにしました。前編ではこの手順のための下準備としてTwilioの使い方を説明します。

概要

  1. まずいつもの通りCloudWatchで監視内容を設定します。
  2. 監視でエラーが発生すると、AWS SNSにアラートが飛ぶようにします。
  3. そのSNSをトリガーとしてAWS Lambdaが起動するようにします。
  4. LambdaはDynamoDBを参照し、エラー通知先の電話番号と通知内容を取得し、Twilio APIを叩いて電話をかけて担当者を叩き起こします。
  5. 担当者は頑張って対応します。

なお、Twilioの全ての機能は有料です。まずクレジットカード(Visa/Masterのみ)を使って事前にいくらか*1料金をチャージしておき、サービスを利用するたびにそこから残高が引き落とされていく仕組みになっています。残高がゼロになるとサービスが利用できなくなりますので適宜追加してください。自動チャージも可能です。

Twilioの準備

アカウント作成

まずはTwilioのトップページより「サインアップ」へ移動してアカウントを作成します。

twilio.kddi-web.com

注意点としては、ここで作ったアカウントは米Twilioのアカウントとは別になるため、サインインする際は https://jp.twilio.com/login/kddi-web (末尾に"kddi-web"がある)を経由する必要があるということです。サインアウトした状態でサービスを使おうとすると https://jp.twilio.com/login にリダイレクトされるのですが、こちらではサインインができません。パスワードのリセットを行おうとしても「このメールアドレスに紐付いたアカウントは存在しない」と言われてしまって混乱することになります。というか、実際に混乱して1時間無駄にしました。

チャージする

アカウントができたら、クレジットカードで適当にチャージします(どうせ使うので)。お試しでもサービスは利用できますが、任意の番号に任意の内容で電話をかけることはできません。

APIクレデンシャルを取得

アカウント取得後、 アカウントページ を見ると、APIを叩くのに必要なキーが表示されています。キーには2種類あり、右側の「テスト」はAPIのテストのために利用できます(これを使ってAPIを叩いても実際に電話をかけるなどの操作は行われません。料金もかかりません)。

f:id:nurenezumi:20160107180340p:plain

発信元電話番号を確認する

電話番号画面を見ると、電話番号が一つ表示されているはずです。この電話番号は自分だけのもので、電話をかける・受けるときに使われます。(有料で)別に新規取得することもできます。最初に確認しておきましょう。

テストしてみる

この情報をもとにエンドポイントにHTTP POSTを送ると電話をかけられます。

ドキュメントの REST API: 通話を開始する を見ると、curlでテスト実行を行うためのサンプルコードが見つかるはずです(「XML」に切り替えるのを忘れないでください)。

f:id:nurenezumi:20160107181519p:plain

既にAPIキーは埋め込まれていますので、{AuthToken}を本物の値に書き換え、Fromを自分の電話番号(上記)に書き換え、さらにToを自分の好きな番号に変更して実行すればいいです。

curl -XPOST https://api.twilio.com/2010-04-01/Accounts/AC92555d**************************/Calls \
    -d "Url=http://demo.twilio.com/docs/voice.xml" \
    -d "To=%2B81311112222" \
    -d "From=%2B8199998888" \
    -u 'AC92555d**************************:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'

発信先電話番号は国番号(日本なら"+81")から指定します。たとえば03-1111-2222にかけるのであれば、先頭のゼロを取り、"+81"を付加して"+81311112222"になります。さらにcurlの仕様により+%2Bエスケープする必要があるので、パラメーターはTo=%2B81311112222と書くことになります。FromはTwilioから発行された電話番号を指定しますが、同様に修正してください。

--data-urlencodeを使えばこの手間は省けますし見やすくなります。

curl -XPOST https://api.twilio.com/2010-04-01/Accounts/AC92555d**************************/Calls \
    --data-urlencode "Url=http://demo.twilio.com/docs/voice.xml" \
    --data-urlencode "To=+81311112222" \
    --data-urlencode "From=+8199998888" \
    -u 'AC92555d**************************:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'

ただのBASIC認証つきのHTTP(S) POSTなので、もちろんPowerShellでも同じように書けます(若干面倒です)。こちらでもエスケープは必要ありません。

$key = "AC92555d**************************"
$secret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
$uri = "https://api.twilio.com/2010-04-01/Accounts/$key/Calls"
$xml = "http://demo.twilio.com/docs/voice.xml"
$parameters = @{Url=$xml; To="+81311112222"; From="+8199998888"; }

# BASIC認証
$auth = "${key}:${secret}"
$base64 = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($auth))
$header = @{Authorization = "Basic $base64"}

Invoke-WebRequest -Uri $uri -Method POST -Body $parameters -Headers $header

うまく行けば電話がかかってきて「Enjoy!」と言ってくれるはずです。

喋る内容をカスタマイズする

日本語で喋らせる

電話で喋る内容は、上記のパラメーターの中で指定した http://demo.twilio.com/docs/voice.xml の中で指定しています。

<?xml version="1.0" encoding="UTF-8"?>
<Response>
    <Say voice="alice">Thanks for trying our documentation. Enjoy!</Say>
    <Play>http://demo.twilio.com/docs/classic.mp3</Play>
</Response>

これはTwiMLと呼ばれるXMLで、文法は TwiMLTM: Twilio マークアップ言語 で解説されています。これと同じようなXMLファイルを自分で用意してWebサーバーでホストし、そのURLを指定すれば任意の文章を読み上げさせることができます。日本語にも対応していて、単にlanguage属性で言語に日本語を指定すればいいだけです。

<?xml version="1.0" encoding="UTF-8"?>
<Response>
    <Say voice="alice" language="ja-JP" loop="3">大切なサーバーが停止しました。</Say>
    <Play>http://s3-ap-northeast-1.amazonaws.com/YOUR-BUCKET-NAME/very-very-horrible-music.mp3</Play>
</Response>

思わず心臓が躍り出すような通告が3回繰り返された後、S3に置いた*2mp3ファイルが再生されます。

TwiMLをS3に配置するときの注意点

TwiMLをS3にホストすればWebサーバーは必要ありませんが、TwilioはデフォルトでこのTwiMLにPOSTでアクセスしに行くため、S3ではエラーになってしまいます*3。パラメーターに Method=GET を追加してこの動作をオーバーライドできます。curlの例であれば--data-urlencode "Method=GET" \を、PowerShellであれば

$xml = "https://s3-ap-northeast-1.amazonaws.com/YOUR-BUCKET-NAME/voice.xml"
$parameters = @{Url=$xml; To="+814155551212"; From="+81012345678"; Method="GET"}

のように書いてください。

Twimletsを使う

いくらサーバーが必要ないとはいえ、あらかじめファイルを作成してアップロードしておくのは面倒なものです。Twilioから提供されているWebサービス「Twimlets」を使うと、上述のTwiMLを動的に生成することができます。

Twilio Labs

パラメーターにXMLの中身を丸ごと渡してあげると、そのままのXMLを出力してくれます。*4

http://twimlets.com/echo?Twiml=%3CResponse%3E%0A%20%20%20%20...

このURLを生成するためのPowerShellのコードを示します。

Add-Type -AssemblyName System.Web
$raw = '<Response><Say voice="alice" language="ja-JP" loop="3">大切なサーバーが停止しました。</Say><Play>http://s3-ap-northeast-1.amazonaws.com/YOUR-BUCKET-NAME/very-very-horrible-sound.mp3</Play></Response>'
$encoded = [System.Web.HttpUtility]::UrlEncode($raw, [System.Text.Encoding]::UTF8)
$xml = "http://twimlets.com/echo?Twiml=" + $encoded

Linuxであればnkfか何かを使ってください。あらかじめ用意することができる固定メッセージであれば上述のURLからブラウザで作ることもできます。このURLさえ準備できれば、Twilioにリクエストを送る際にUrlオプションにこのURLを渡してあげればいいわけです。なお、curlでリクエストを送る場合は-dではなく--data-urlencodeオプションを使ってください。

Lambda(Node.js)から呼ぶ

ライブラリも提供されているのですが、この程度であれば直接リクエストを送ってしまった方が早いでしょう。次のコードはLambdaで実行すると電話をかけて処理を終了します。

var https = require('https');

exports.handler = function(event, context) {
    var key = "AC92555d**************************"
    var secret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    var options = {
        hostname: "api.twilio.com",
        port: 443,
        path: "/2010-04-01/Accounts/" + key + "/Calls",
        method: 'POST',
        auth: key + ":" + secret,
        headers : { "Content-Type" : "application/x-www-form-urlencoded" }
    };

    var req = https.request(options, function(res) {
        if (200 <= res.statusCode && res.statusCode <= 204) {
            console.log("OK!")
            context.succeed();
        } else {
            console.log("HTTP status code is NOT 2xx but " + res.statusCode);
            res.setEncoding("utf8");
            res.on('data', function(data) {
                console.error(data);    
                context.fail(res);
            });
        }
    });
    
    req.on('error', function(e) { context.fail(e.message); });

    var rawXml =
        '<?xml version="1.0" encoding="UTF-8"?>' +
        '<Response>' +
        '  <Say voice="alice" language="ja-JP" loop="3">大切なサーバーが停止しました。</Say>' +
        '  <Play>http://s3-ap-northeast-1.amazonaws.com/YOUR-BUCKET-NAME/very-very-horrible-music.mp3</Play>' +
        '</Response>';
    var urlParam = encodeURIComponent('http://twimlets.com/echo?Twiml=' + encodeURIComponent(rawXml));

    req.write("To=+81311112222&");
    req.write("From=+8199998888&");
    req.write("Method=GET&");
    req.write("Url=" + urlParam);
    req.end();
};

注意点としては、

  • HTTP POSTでパラメーターを送信するので、ヘッダにContent-Type : application/x-www-form-urlencodedを指定する
  • XMLの中身はencodeURIComponentでURLエンコードしてTwimletsに渡す
  • そのURL自体をまとめて再度encodeURIComponentでURLエンコードしてTwilioに渡す

といったあたりです。

まとめ

bashPowerShellのコンソールと、電話という子供の頃から慣れ親しんできたデバイスとが繋がるのはなんだか不思議な感じがします。次回はこちらを使い、CloudWatch+Lambdaから呼び出して実際に電話をかけることにします。

*1:2,000円以上、1,000円単位になります

*2:Twilioのサーバーが取得できるように外部からアクセス可能でないといけません

*3:S3はHTTP POSTを受け取ると無条件でエラーを返す仕様となっています

*4:メッセージ内容だけを渡すバージョンもあるのですが、language属性の指定ができないため日本語では使えませんでした

Google Chromeアプリ[Postman]の使い方メモ

井上です。WEB API作成時にとても便利なPostman。その使い方についてです。

Postmanとは

Google Chromeのブラウザ上からWEB APIを呼び出すことができるアプリです。
公開APIを用いたシステムを開発の際に実際に呼び出して確認したり、自社で提供するWEB APIを作成した際にドライバ替わりに使ったりできるので開発者にとって大変利便性の高いアプリとなっています。

Postman

f:id:ihisa:20160125192828p:plain

実際に公開APIを呼んでみる!

郵便番号検索APIに記載のリクエスト例をPostmanで呼んでみます。
f:id:ihisa:20160127182527p:plain

美しく結果が返ってきました。楽ちん!
APIと言ってますがつまりはHTTPリクエストなので、いつもご覧のページを呼んでみると楽しいかもしれません。

Postmanを便利に使う!

URL入れてパラメータセットして呼ぶだけでも十分便利なのですが、せっかくなのでもっと活用しましょう。特にコレクションとテスト機能は便利ですので是非使いましょう。

コレクションの活用

よく使うものをコレクションに登録する

SendしまくってるとHistoryがどんどんたまっていきます。いつも使うものはブックマークできたらいいですよね。

Historyで履歴を選択すると[Add to collection]と表示されますので押下します。
f:id:ihisa:20160126090841p:plain

登録したいフォルダを作成/選択します。
f:id:ihisa:20160125203743p:plain

登録されます。
f:id:ihisa:20160126172218p:plain

説明文を追加する

urlだけだと目的が分からなくなりがちなので、説明を入れましょう。
対象のコレクションにカーソルをあてると[・・・]アイコンが出ますので選択→editです。
f:id:ihisa:20160126171806p:plain

Request Descriptionに説明文を入力します。 f:id:ihisa:20160126171923p:plain

コレクションから対象を選ぶとurlの下に先ほど入力した説明文が表示されています。
f:id:ihisa:20160126172052p:plain

登録したコレクションを更新する

対象のコレクションを選択・更新した後、右側フロッピー的なアイコンを押すと更新できます。
f:id:ihisa:20160126090752p:plain

コレクションをダウンロード / アップロード

メンバーが作ったコレクションを共有したい場合に使えます。

ダウンロードはダウンロードしたいコレクションの[・・・]アイコン押下→[Download]、もしくはその上の矢印アイコン押下で[Run]ボタンから右4つめのアイコンでダウンロードできます。
f:id:ihisa:20160126090652p:plain

アップロードは画面上の[Import]から実行します。ダウンロードしたファイルをアップすればコレクションに追加されます。
f:id:ihisa:20160126090916p:plain

テスト

期待値チェック

いろいろ期待値をチェックできます f:id:ihisa:20160126091030p:plain [Tests]タブを選ぶとSNIPETTSが右側に出てきますので、選択することで左のテキストエリアに設定されます。自身でテストコードを書くこともできます。
詳しくはWriting tests | Postman Learning Center

設定した後[Send]しますと、[Tests]タブに結果がバッチリ入っています。素晴らしい! f:id:ihisa:20160126091310p:plain

一気にテスト実行

対象のコレクションを選ぶと[Run]ボタンが出てきますのでここから実行します。 f:id:ihisa:20160125204936p:plain

f:id:ihisa:20160125205007p:plain

Postmanを使って効率いい開発ライフを送りましょう。

AWS LambdaのパラメータをDynamoDBで管理する

こんにちは、AWS担当のwakです。前回に続きAWS Lambdaの話をします。

f:id:nurenezumi:20160127153847j:plain

【インデックス(予定)】

  1. CloudWatchの監視結果をSlackに流す(AWS Lambdaバージョン)
  2. AWS LambdaでWebサイトの死活監視を行ってSlackに結果を流す
  3. AWS LambdaのパラメータをDynamoDBで管理する(今回)
  4. CloudWatchの監視結果をTwilioに流して電話をかける
  5. SlackからEC2インスタンスを起動・停止するコマンドを作る

パラメーターをDynamoDBに入れて管理する

前回までの記事でCloudWatchの監視結果やWebサイトの死活監視の結果をSlackへ通知するようなコードを書きましたが、これらで使っているパラメーターはコードの中に直接埋め込んでいました。この改善を図るためDynamoDBを使いたいと思います。レコード数が十分少ない場合は検索は単純な処理で可能ですので、詳しくは補足記事を参照してください。DynamoDBでは課金が発生しますが、後述する通り通常は無料利用枠におさまるため、無料~極めて安価に利用できます。

登録するパラメーターを決める

パラメーターを用意します。Slack連携を行うために必要な情報を書いてみました。

  • incoming webhooksのエンドポイント
  • 通知先チャンネル(正常時/エラー時)
  • botの名前
  • アイコン

これらをまとめたものに一意の名前(ID)を振ってDynamoDBで管理することにします。

DynamoDBの準備

テーブルを作成する

適当な名前でDynamoDBにテーブルを作ります。プライマリキー名は文字列型のrecordkeyとしました。keyのような予約語を使うこともできますが、コードが少しだけ面倒になります。

レコードを入れる

コンソールから手動でレコードを投入します。直接JSON形式で書いて保存してしまいます。

{
  "recordkey": "slack",
  "type": "slack",
  "channel": {
    "error": "@notification_error",
    "ok": "notification_ok"
  },
  "emoji": ":ghost:",
  "endpoint": "/services/xxxxxxxxx/xxxxxxxxx/xxxxxxxxxxxxxxxxxxxxxxxx",
  "name": "MYSLACKBOT"
}

このテーブルからrecordkeyslackのレコードを検索すればSlack通知を行うためのパラメーターを取得することができるようになるわけです。もちろんslack2GoogleAPIのようなレコードがあっても構いません。

Lambdaの準備

以降の操作はLamndaで行います。

ロールを作成する

Lambda関数はそれぞれの関数に割り当てられたIAMロール*1の権限で実行されます。これにより、APIキーの管理やローテーションといった面倒な作業は必要なくなります。

今回のLambda関数はDynamoDBを読み込むため、DynamoDBへのアクセス権限を持ったロールを作成して割り当てる必要があります。幸いテンプレートが用意されているので、コンソールから選択して作成するだけで済みます。

目的のレコードを検索する

Slackのパラメーターを読み出すには、recordkey = 'slack'で検索をすれば良かったのでした。これで成功時には必ず1件のレコードが取得できるはずです。

var param = {
    TableName : "helloLambda2",
    KeyConditionExpression : "recordkey = :val",
    ExpressionAttributeValues : {":val" : "slack"}
};
dynamo.query(param, function(err, data) { ... }

読み込みが失敗(権限が足りない、テーブルが見つからない、期待したレコードが見つからないなど)した場合はその場でエラーを返し、成功した場合は続けて任意の処理を行ってSlackへ通知するようにすれば終わりです。

dynamo.query(param, function(err, data) {
    if (err) { context.fail(err); }
    else if (data.Count === 0) { context.fail("record not found"); }
    else {
        var slackParameter = data.Items[0]; // 登録したのと同じ形のオブジェクト
        // do something
        context.succeed({result : "ok", message : "..."});
    }
});

料金見積もり

DynamoDBではいくつかの名目で料金が発生しますが、無料利用枠として、1アカウントあたり

  • ストレージ……25GB
  • プロビジョニングした読み込み/書き込み容量……25ユニット

が用意されています。EC2やRDSの無料利用枠はサインアップ(アカウント作成)後から1年間限定の特典なのですが、DynamoDBではその後もずっと継続して与えられます。この程度(レコード数100未満、1件のレコードは1KB未満)であれば、この無料利用枠に十分おさまる、すなわち完全に無料で使えると見て良いでしょう。また、読み込み/書き込み容量が無料枠を超過したとしても、それぞれ1ユニット(つまり秒間1回ずつの読み込みと書き込みが可能)で十分ですから、料金は最低額の約$0.67/月となります。

*1:IAMユーザーとIAMロールはそれぞれ別物です。IAMユーザーは人間がブラウザ経由で操作する・アプリケーションがAPIキーを使って操作するためのユーザーアカウントです。IAMロールはAWS内部のサービスが他サービスを利用するための権限のセットです

Node.js+Dynamo DBでレコードを検索する

こんにちは、AWS担当のwakです。

前回前々回の記事ではLambdaからSlackへの通知を行いましたが、そこではスクリプト内部にパラメーターをハードコードしていました。これを改善するためにDynamoDBを使おうと思ってコードのサンプルを探してみたのですが、古い記事や新しい記事が入り交じっていて何が最新なのかよく分からなくなってきたためまとめることにします。

f:id:nurenezumi:20160119164826j:plain

この記事の内容

AWS Lambda(Node.js)からSDKを使ってDynamoDBを利用する際、クライアントオブジェクトを生成するためのやり方がいくつかあります。その内容について説明しています。

前提条件

以下のコードは全てDynamoDBにアクセスする権限のあるロールでLambdaから実行しています。また、DynamoDBには事前にテーブルを作成し、数レコードのデータを投入してあるものとします。

f:id:nurenezumi:20160118182845p:plain


f:id:nurenezumi:20160118182926p:plain

DocumentClientを使う

もともとDnyamoDBをJavaScriptから利用するためのSDKとしてはAWS.DynamoDBというクラスがあるのですが(後述)、検索時に値の型を明示する必要があったりしてあまり使いやすいとは言えません。たとえば検索時や新規レコード挿入時に"ABC"という文字列を与えるのにもいちいち{"S" : "ABC"}というハッシュ(Sは文字列を意味するラベル)として渡す必要がありますし、逆に検索結果に"ABC"という文字列が含まれていても、結果は{"S" : "ABC"}というハッシュで返ってきます。

AWS.DynamoDB.DocumentClientはこの辺りを改善し、より簡単にAPIを利用するためにSDKを呼び出すラッパーライブラリです。2015年10月にバージョン2.2.0がリリースされました

次のコードはscan()メソッドを使い、指定したテーブルにある全レコード*1を取得してログへ出力します。

var AWS = require("aws-sdk");
var dynamo = new AWS.DynamoDB.DocumentClient();
exports.handler = function(event, context) {
    dynamo.scan({TableName : "TABLENAME"}, function(err, data) {
        if (err) {
            context.fail(err); // エラー時
        } else {
            context.succeed(data); // 正常時
        }
    });
};

条件を指定して検索を行うには何通りかの方法があります。

scan()で検索

scan()に渡すパラメーターに条件を追加してあげれば検索ができます。たとえばSQLSELECT * FROM TABLENAME WHERE num = 10 に相当する検索を行いたければこうなります。

var param = {
    TableName : "TABLENAME",
    FilterExpression : "num = :val",
    ExpressionAttributeValues : {":val" : 10}
};
dynamo.scan(param, function(err, data) { ... }

つまり、

  • FilterExpressionプレースホルダーを埋め込んだ条件式を与える
  • 実際の値をExpressionAttributeValuesで与える

と検索ができる、というだけの簡単な仕組みです。どのキーでも検索対象とできますが、文字通りのスキャンなので検索コストが跳ね上がる可能性があります。インデックスを追加して検索に使えるようにしてあげるというお馴染みの手法で改善を図れます。

query()で検索・その1

scan()のかわりにquery()を呼んでも検索ができます。ただし今度の検索条件にはプライマリキーの指定が必須となります(不自由な分だけ低コストです)。たとえばSQLSELECT * FROM TABLENAME WHERE key = 'A' に相当する検索ならこうなります。

var param = {
    TableName : "TABLENAME",
    KeyConditionExpression : "#k = :val",
    ExpressionAttributeValues : {":val" : "A"},
    ExpressionAttributeNames  : {"#k" : "key"}
};
dynamo.query(param, function(err, data) { ... }
  • KeyConditionExpressionプレースホルダーを埋め込んだ条件式を与える
  • 実際の値をExpressionAttributeValuesで与える
  • 検索するプライマリキー・セカンダリキー名にkeyFILEといった予約語 を使っている場合はエラーになる
    • #から始まる適当な別名(上の例では#k)を付けて、その実際のキー名をExpressionAttributeNamesで与えることで回避できる

プライマリキーが条件に含まれていれば、セカンダリキーを追加で指定することもできます。

var param = {
    TableName : "TABLENAME",
    KeyConditionExpression : "#k = :val and key2 >= :val2",
    ExpressionAttributeValues : {":val" : "A", ":val2" : "Q"},
    ExpressionAttributeNames  : {"#k" : "key"}
};
dynamo.query(param, function(err, data) { ... }

query()で検索・その2

query()KeyConditionExpressionではなくKeyConditionという別の形のパラメーターも受け付けます。ただ、こちらは冗長で読みづらいだけでなく、ドキュメントに「古いやり方なのでもう使うな」とあるため使用は避けるべきでしょう*2。上と同じ検索を行うコードは次の通りです。

var param = {
    TableName : "TABLENAME",
    KeyConditions : {
        key : {
            ComparisonOperator : "EQ",
            AttributeValueList : {S : "A" }
        },
        key2 : {
            ComparisonOperator : "GE",
            AttributeValueList : {S : "Q" }
        }
    }
};

get()で検索

get()はレコード1件だけを取得するためのメソッドです(内部的にはAWS.DynamoDB.getItem()を呼び出します)。つまり検索条件からレコードを特定できないといけないので、プライマリキーの指定は必須、テーブルにセカンダリキーが存在する場合はセカンダリキーの指定も必須になります(そうしないと"The provided key element does not match the schema"と言われてエラーになります)。パラメーターの形はだいぶ違いますがその分だけシンプルになります。

var param = {
    TableName : "TABLENAME",
    Key: {
        key : "A",
        key2: "Q"
    }
};
dynamo.get(param, function(err, data) { ... }
  • 大文字のKeyは検索条件を示します。
  • 小文字のkey, key2はテーブルのプライマリキー・セカンダリキーの名前です。

batchGet()で検索

内部的にはAWS.DynamoDB.batchGetItem()を呼び出します。検索レコード数が多いときなど、ページングを行うにはこちらのメソッドを使う必要があります。今回は省略します。

aws-sdkを使う

上述のAPI、つまりAWS.DynamoDBを直接呼び出すこともできます。全件検索はこうなります。

var AWS = require("aws-sdk");
var dynamo = new AWS.DynamoDB();
exports.handler = function(event, context) {
    dynamo.scan({TableName : "TABLENAME"}, function(err, data) {
        if (err) {
            context.fail(err); // エラー時
        } else {
            context.succeed(data); // 正常時
        }
    });
};

2行目を除いてDocumentClientの例と全く同じです。ただし結果の形式が異なり、フィールドの型が明示的に指定されます。ドキュメントの中でも言及されている通り、数値(整数・実数)の区別があるため厳密ですが、状況によっては使いづらいかもしれません。

{
  "Items": [
    {
      "key": {
        "S": "A"
      },
      "key2": {
        "S": "P"
      },
      "str": {
        "S": "ap"
      },
      ...

現時点でAPIのバージョンには2012-08-10版と2011-12-05版の2種類があり、どちらかを明示的に指定することもできますが、古い方をあえて指定するメリットはなさそうです。省略すれば新しい方になります。

var AWS = require("aws-sdk");
var dynamo= new AWS.DynamoDB({apiVersion: '2012-08-10'});

scan()で検索

FilterExpressionが使えます。

var param = {
    TableName : "TABLENAME",
    FilterExpression : "num = :val",
    ExpressionAttributeValues : {":val" : {N : "10"}} // 数値の10を指定するときの書き方
};
dynamo.scan(param, function(err, data) { ... }

ScanFilterも使えます。

var param = {
    TableName : "TABLENAME",
    ScanFilter : {
        num : {
            ComparisonOperator : "EQ",
            AttributeValueList : [{N : "10"}]
        }
    }
};
dynamo.scan(param, function(err, data) { ... }

query()で検索

ドキュメントにはKeyConditionExpressionが使えると書いてあるのですが、エラーになってうまくいきませんでした。KeyConditionsなら動きます(ただしDocumentClient版とは微妙にパラメーターの形が違います。AttributeValueListには配列を渡さなければなりません)。

var param = {
    TableName : "TABLENAME",
    KeyConditions : {
        'key' : {
            ComparisonOperator : "EQ",
            AttributeValueList : [{S : "A"}] // ←配列
        },
        'key2' : {
            ComparisonOperator : "GE",
            AttributeValueList : [{S : "Q"}] // ←配列
        }
    }
};
dynamo.query(param, function(err, data) { ... }

getItem()で検索

DocumentClientと同じ形で検索条件を渡せば同じ結果が返ってきます。

var param = {
    TableName : "TABLENAME",
    Key: {
        key : {S : "A"},
        key2: {S : "Q"}
    }
};
dynamo.getItem(param, function(err, data) { ... }

dynamodb-docを使う

Lambdaのblueprintで"DynamoDB"が名前に付くものを探すといくつか候補が出てきますが、この中で使われているものです。

f:id:nurenezumi:20160118161954p:plain
var doc = require('dynamodb-doc');
var dynamo = new doc.DynamoDB();
exports.handler = function(event, context) {
    dynamo.scan({TableName : "TABLENAME"}, function(err, data) {
        if (err) {
            context.fail(err); // エラー時
        } else {
            context.succeed(data); // 正常時
        }
    });
};

dynamodb-document-js-sdk だと思われますが、こちらはNOTEに記載されている通り今後はメンテナンスされません。使わない方が無難だと思われます。

まとめ

DynamoDBのAPIはずいぶん長い間放っておかれていたように思っていたのですが、そのせいか古い情報がまだ数多く残っています。ドキュメントを熟読するのは大切だなあと思った一コマでした。

*1:サイズ制限があり、それを超えた分はページングで取得する必要がありますが、今回は省略します

*2:なおDocumentClientのドキュメントにはKeyConditionExpressionの説明しか載っていません。KeyConditionの説明はAWS.DynamoDBのドキュメントにあります

AWS LambdaでWebサイトの死活監視を行ってSlackに結果を流す

こんにちは、AWS担当のwakです。前回に続きAWS Lambdaの話をします。

f:id:nurenezumi:20160104165124j:plain

良い角度を知っている猫

【インデックス(予定)】

  1. CloudWatchの監視結果をSlackに流す(AWS Lambdaバージョン)
  2. AWS LambdaでWebサイトの死活監視を行ってSlackに結果を流す(今回)
  3. DynamoDBにパラメーターを入れてハードコードをなくす
  4. CloudWatchの監視結果をTwilioに流して電話をかける
  5. SlackからEC2インスタンスを起動・停止するコマンドを作る

今回の目的について

前回に続いて、Lambda (Node.js)を使ってWebサイトの監視を行います。

  • 任意のWebサイトにアクセスし、HTTP 200が返るかどうかを確かめる
  • その結果をSlackで通知する
  • この処理をN分おきに実行する(cron形式でスケジュールを設定可能)

これに似たサービスはいくらでもあるのですが、

  • cron形式でスケジュールを設定可能
  • 通知先はSlackに限らない
  • AWS Lambdaで実行されるため、サーバー障害などにも非常に強い
  • 安価
  • AWS Lambdaと仲良くなれる

といった利点があります。それではさっそく始めましょう。

Slackの準備をする

Slack側で通知を行うためにIncoming WebHooksの準備をします。こちらは前回と同じ手順で新規作成しても構いませんし、新規に作っても構いません。

Lambdaの準備をする

Management ConsoleからLambdaを選ぶと、最初にブループリント(テンプレートみたいなものですね)の選択画面が表示されます。試しに「https-request」を選ぶと、Node.jsでHTTPSアクセスを行うためのサンプルコードが入力された状態でコード編集画面へと遷移します。

f:id:nurenezumi:20151218165730p:plain

コードを入力する

元から記入されているコードを全て削除し、置き換えます。完全な形のコードはこの記事の末尾に掲載しているのでコピペしてください。sendToSlack()は前回と同じもので、Slackへ通知を行ってから処理を終了する関数です。

var https = require('https');
var http = require('http');

function sendToSlack(path, message, channel, context, otherParameters) {
    ...
}

exports.handler = function(event, context) {
   ...
}

テスト実行する

eventの値は見ていないので、適宜コードを修正して監視対象のURLとページ名(Slack通知時に使われます)、Slackのエンドポイント・通知チャンネル名を変更してテスト実行してみましょう。設定がうまく済んでいれば通知が行われるはずです。

実行設定する

Event sourcesタブを開き、"Add event source"をクリックします。"Event source type"に"Scheduled Event"をセットすると、「5分ごと」「15分ごと」のようなプリセットの他に、cron形式で入力ができるようになります。

公式ドキュメント を読めば特に難しいことはないのですが、たとえば「月曜日から金曜日の毎時5分、15分、……55分」というスケジュールで定期実行するように設定するのであればこうなります。

f:id:nurenezumi:20151225180849p:plain

cron(5/10 * ? * MON-FRI *) が指定している内容で、パラメーターは順に

  1. 5/10は毎時5分、15分、……、55分を示す書式です。0/5なら0分、5分、10分、……になります。0なら毎時0分になります。
  2. 次の *全ての時(hour) を指定しています。
  3. 次の ? は日を 指定しない ことを意味しています。ここで*(全ての)を指定してしまうと、後の曜日指定と矛盾してしまうのでエラーになります(ちょっとハマりました)。
  4. 次の * は全ての月を指定しています。ここは良いでしょう。
  5. 次のMON-FRIは曜日です。MON,WED,FRIのように列挙することもできます。
  6. 最後の * は全ての年です。

ですから、たとえば「毎日9時から18時の間、毎時0分と30分」であればcron(0,30 9-18 * * ? *)になります(今度は曜日の方を「指定しない」としています)。

ここで登録したスケジュールは、AWS Lambdaとは独立したリソースとして保存・管理されます*1。別のLambda functionで使い回すこともできるようになっています。

まとめ

作ろうと思ったら簡単に実装ができてしまいました。次回はソースコードの中の値をDynamoDBに格納し、ハードコードを排除してみます。

コード

var https = require('https');
var http = require('http');

/*
 * Slackに通知を行う。
 * path: Slackのエンドポイント。 "/services/xxxxxxxxx/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" の形式の文字列
 * message : 送信するメッセージ
 * context : Lambdaのコンテキスト
 * otherParameters : icon_emoji, usernameなどをキーに持った配列(省略可能)
 */
function sendToSlack(path, message, channel, context, otherParameters) {
    context = context ? context : {succeed: function(){}, fail: function(){}, done: function(){}};
    
    var options = {
        hostname: "hooks.slack.com",
        port: 443,
        path: path,
        method: 'POST'
    };

    var req = https.request(options, function(res) {
        if (res.statusCode == 200) {
            context.done();
        } else {
            var message = "通知に失敗しました. SlackからHTTP " + res.statusCode + " が返りました";
            console.error(message);
            context.fail(message);
        }
    });
    req.on('error', function(e) {
        var message = "通知に失敗しました. Slackから次のエラーが返りました: " + e.message;
        console.error(message);
        context.fail(message);
    });
    
    var parameters = otherParameters ? otherParameters : {};
    parameters.text = message;
    parameters.channel = channel;
    req.write(JSON.stringify(parameters));
    req.write("\n");
    req.end();
}

exports.handler = function(event, context) {
    console.log(event);
    
    // 証明書エラーを無視する
    process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";

    var url = "https://www.google.co.jp";
    var title = "Googleトップページ";
    var path = "/services/xxxxxxxxx/xxxxxxxxx/xxxxxxxxxxxxxxxxxxxxxxxx";
    var channelName_error = "#channelNameError"; // エラー時の通知先チャンネル名。必須
    var channelName_ok = "#channelNameOk"; // OK時の通知先。空文字にするとOK時は通知が出なくなる
    var option = { icon_emoji : ":ghost:", username : "MYBOT" };
    
    var agent = (url.indexOf("https://") === 0) ? https : http;
    agent.get(url, function(res) {
        console.log(res);
        var message = title + ": " + url + " にアクセスして HTTP " + res.statusCode + " が返ってきました";
        var channel = (res.statusCode == 200) ? channelName_ok : channelName_error;
        if (!channel) {
            console.log(message + ". 通知はスキップします");
            context.done();
            return;
        } else {
            console.log(message);
            sendToSlack(path, message, channel, context, option);
        }
    }).on('error', function(e) {
        var message = url + "にアクセスできませんでした:\n" + e.message;
        console.error(message);
        sendToSlack(path, message, channelName_error, context, option);
    });
};
  • sendToSlack()は前回からの使い回しです。
  • 前回に続き、処理はSlack通知をもって完了します。
    • そのため、context.succeed()context.failsendToSlack()の中だけで呼んでいます。
    • それより前に呼んでしまうと、(非同期で)コンテンツを取得しに行く前に処理が終わってしまいます。

*1:そのため、スケジュール保存時に単独でARNが振られます

CloudWatchの監視結果をSlackに流す(AWS Lambdaバージョン)

こんにちは、AWS担当のwakです。

自分が書いた過去の記事を読み返していたところ、

tech.sanwasystem.com

AWS Lambdaが使えるようになった今となっては、こちらの記事の内容は既に用済みになっていることに気付きました。今回は表題通り、連携サーバーのかわりにAWS Lambdaを使ってCloudWatchの監視結果をSlackへ流すための方法について書くことにします。

f:id:nurenezumi:20151222162240j:plain

監視猫(watchcat)

AWS Lambdaについて

AWS Lambdaとは、JavaScript(Node.js)、PythonJavaで書いた任意のコードをAWS上で実行するための仕組みです。「AWS上で」とは、つまり「EC2インスタンスを含むサーバーなしで」を意味し、課金はコードを実行するために消費されたCPU時間に応じて行われます。具体的なユースケースは非常に多岐にわたりますが、今回のような監視・連携サーバーを置き換え、料金と管理のコストを抑えるのにもぴったりのサービスです。

仕組み

f:id:nurenezumi:20151222160543p:plain

前(連携サーバーを使うバージョン)

f:id:nurenezumi:20151222160609p:plain

後(Lambdaに切り替えたバージョン)

こうなります*1。以前と比べて連携サーバーを使う必要がなくなったため、

  • シンプルになった(SQSも消えた)
  • サーバー障害時のことを気にしなくてよくなった
  • 安くなった

といったメリットがあります。

手順

手順概要

  1. SlackでIncoming WebHooksの設定をする
  2. SNSのトピックを作る
  3. CloudWatchの設定を行い、エラー時にSNSへ通知が送られるようにする
  4. Lambdaでコードを書く
  5. SNSをトリガーとしてLambda functionを起動するように設定する
  6. テスト実行する

次に詳細を書いていきます。

1. SlackでIncoming WebHooksの設定をする

外部からSlackにメッセージを送信するための下準備をします。 Add Apps to Slack | Apps and Integrations | Slack App Directory にアクセスし、"Build Your Own"を選んで、Make a Custom Integration→Incoming Webhooksと進み、通知のために使うIncoming Webhooksを作成します(既存のものを使い回すならそれでも構いません)。作成すると、メッセージを送信するためのエンドポイントが表示されます。このURLを確認しておきます。

f:id:nurenezumi:20151221102606p:plain
Incoming WebHooksのテスト

Incoming WebHooksができたら送信テストをしましょう。Incoming WebHooksの編集画面を見ると、上のSetup Instructionsの部分にcurlでテストメッセージを送信するためのコマンドが表示されています。

f:id:nurenezumi:20151221103403p:plain

正しく設定ができていれば、これをそのまま実行するとメッセージが通知されるはずです。PowerShell版も用意しました。

$utf8 = New-Object System.Text.UTF8Encoding
$body = @{channel = "チャンネル名"; text = "メッセージ"} | ConvertTo-Json # 直接JSON形式の文字列で書いてもOK
Invoke-WebRequest https://hooks.slack.com/services/xxxxxxxxB/xxxxxxxxx/xxxxxxxxxxxxxxxxxxxxxxxx -Method POST -Body $utf8.GetBytes($bodyJson)

チャンネルのかわりに自分へのDMを送りたければ、チャンネル名には@usernameのように書いてください。

2. SNSのトピックを作る

CloudWatchがメッセージを送るためのトピックを作成します。後が簡単になるので、これは「エラーが発生したときのためのトピック」ということにします。*2

f:id:nurenezumi:20151221101311p:plain

このトピックはCloudWatchとLambda functionとの間をつなぐためのものなので、トピック側での設定は不要です。もちろん必要があればSQSやメールを流すようにしても構いません。

3. CloudWatchの設定を行い、エラー時にSNSへ通知が送られるようにする

適当にCloudWatchのメトリクスを作成し、何かの条件を満たしたら通知が送られるようにします。

f:id:nurenezumi:20151221104951p:plain

4. Lambdaでコードを書く

AWS Lambdaを開きます。「Create a Lambda function」ボタンで進み、blueprint画面は「Skip」ボタンでスキップして空のLambda function作成画面を表示します。名前と説明は適当に入力し、この記事の末尾に示すコードを入力します。

ロールの登録

AWS Lambdaを初めて使う場合、このfunctionを実行するためのロールが存在しないかもしれません。画面の指示に従って新規作成しておきます。いくつか選択肢がありますが、今回はAWSのリソースには触れないので、"Basic execution role"*3を選んでおけば十分です。

f:id:nurenezumi:20151221161853p:plain

5. SNSをトリガーとしてLambda functionを起動するように設定する

Lambda functionのEvent sourcesタブに移動し、Add event sourceをクリックすると、起動する契機となるイベントの種類を選択できます。SNSを選択し、先ほど作成したトピックを選べば完了です。

f:id:nurenezumi:20151221163125p:plain

6. テスト実行する

APIを叩いてCloudWatchのメトリクスをテスト実行するか、設定の方を変更して監視の条件を満たすようにします。するとほぼ即座にLambda functionが実行され、Slackに通知が飛んでくるはずです。

もしうまく行かない場合は、Lambda functionの実行ログがCloudWatchに残っているので確認してみてください。この「ログ」でLambda functionのログストリームをチェックすることができます。

f:id:nurenezumi:20151221163934p:plain

まとめ

Amazon LambdaはAWSがプッシュしているだけのことはあり、ずいぶんと機能が拡張されていました。非同期処理のおかげで多少戸惑ったりハマったりしましたが(下の注意書き参照)、一度動いてしまえばこっちのものです。今年の漢字は「安」だそうですが、これを使って安全・安定・安価な監視を実現して1年を締めくくろうと思いました。

コード

完全なコードは以下の通りです。

var https = require('https');

/*
 * Slackに通知を行う。
 * path: Slackのエンドポイント。 "/services/xxxxxxxxx/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" の形式の文字列
 * message : 送信するメッセージ
 * context : Lambdaのコンテキスト
 * otherParameters : icon_emoji, usernameなどをキーに持った配列(省略可能)
 */
function sendToSlack(path, message, channel, context, otherParameters) {
    context = context ? context : {succeed: function(){}, fail: function(){}, done: function(){}};
    
    var options = {
        hostname: "hooks.slack.com",
        port: 443,
        path: path,
        method: 'POST'
    };

    var req = https.request(options, function(res) {
        if (res.statusCode == 200) {
            context.done();
        } else {
            var message = "通知に失敗しました. SlackからHTTP " + res.statusCode + " が返りました";
            console.error(message);
            context.fail(message);
        }
    });
    req.on('error', function(e) {
        var message = "通知に失敗しました. Slackから次のエラーが返りました: " + e.message;
        console.error(message);
        context.fail(message);
    });
    
    var parameters = otherParameters ? otherParameters : {};
    parameters.text = message;
    parameters.channel = channel;
    req.write(JSON.stringify(parameters));
    req.write("\n");
    req.end();
}

// エントリーポイント
exports.handler = function(event, context) {
    for(var i = 0;  i < event.Records.length; i++) {
        var record = event.Records[i];
        var sns = record.Sns;
        var message = JSON.parse(sns.Message);
        
        var text = [];
        text.push("次のエラーが発生しました:");
        text.push("名前 : " + message.AlarmName);
        text.push("内容 : " + message.AlarmDescription);
        text.push("理由: " + message.NewStateReason);
        
        //message, channel, context, otherParameters
        console.log(text.join("\n"));
        sendToSlack(
            "/services/xxxxxxxxx/xxxxxxxxx/xxxxxxxxxxxxxxxxxxxxxxxx",
            text.join("\n"),
            "CHANNELNAME",
            i == event.Records.length - 1 ? context : null,
            {"icon_emoji" : ":ghost:"}
        );
    }
}
  • 最初に実行されるのはexports.handler()です。*4
  • 引数のうち、eventは呼び出し元が作成するパラメーターのオブジェクトです。
    • 今回の設定ではSNSが作成するオブジェクトです。アラームの内容などはJSON形式のテキストで格納されているためにJSON.parse()を呼んでいます。
    • 手動実行する場合は自分で書いたJSONを渡せます。
  • contextはこの関数の結果をAWSに通知するためのコンテキストです。 公式ドキュメント
    • context.succeed(), context.fail()を呼ぶとそこで処理が終了します。
      • したがって、処理途中でこのいずれかを呼ぶと非同期のSlack API呼び出しが中断してしまいます。
    • context.succeed()を明示的に呼ばずに関数を抜けると失敗扱いになります。

おまけ: CloudWatchから渡される値

SNS+CloudWatchから起動されると、eventには次のような値が渡されます。

{
  Records: [{
    EventSource: 'aws:sns',
    EventVersion: '1.0',
    EventSubscriptionArn: '...',
    Sns: {
      Type: 'Notification',
      MessageId: '...',
      TopicArn: 'arn:aws:sns:ap-northeast-1:xxxxxxxxxxxx:YOUR-METRICS-NAME',
      Subject: '...',
      Message: '《JSON形式の文字列》',
      ...
    }
  }]
}

JSON形式の文字列」をJSONとしてパースするとこうなります。

{
  AlarmName : "メトリクスの名前(初回設定から変更不可能)",
  AlarmDescription : "メトリクスの説明(変更可)",
  AWSAccountId : "999999999999",
  NewStateValue : "OK","
  NewStateReason : "なぜstateが変わったかの理由",
  ...
}

確認した範囲ではRecordsが2件以上の要素を持った配列になることはありませんでした(が、対応は必要でしょう)。

*1:SNS Topicが複数あるのは、エラーのレベルによって通知先を変更したりすることができるようにするためです

*2:「エラーから復帰したときのトピック」も別に作り、今回と同様のことをすれば、エラーから復帰したときにもSlack通知を行うことができます。手順は全く同様なので今回は省略します

*3:CloudWatchに実行ログを登録する権限だけが付与されています

*4:デフォルトでそのような設定になっているため。変更することもできます