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:デフォルトでそのような設定になっているため。変更することもできます

Oracle12C プラガブル・データベースの自動起動設定

こんにちは。山内です。
この前、Oracle12Cを開発用にPCへインストールしたのですが、インストールした翌日にPCを起動して開発を始めたところ、データベースに接続できない状態が発生いたしました。調査したところ、Oracle12Cのプラガブル・データベース(以下:PDB)は デフォルトではサーバーやPC起動時には自動で起動するようになっていないため、サーバーやPC起動時に手動で起動を行うか自動で起動するように設定が必要であることが判明しました。 同じようなことで困った人もいるかと思いますので、サーバーやPC起動時にPDBが自動で起動するように設定した方法を以下に記載いたします。


環境

Oracle12.1.0.2とOSはWindowsを使用することを前提に説明を行います。


PDBについて

Oracle Database 12cで機能追加されたのが、「マルチテナント・アーキテクチャ」です。マルチテナント・アーキテクチャは、マルチテナント・コンテナ・データベース(以下:CDB)というデータベースに含まれる1つ以上のPDBによって構成されます。スキーマや表領域やデータなどがセットになったものがPDBです。アップグレードやパッチ適用はCDBに適用を行うことでCDBに複数のPDBを構成している場合は1度の適用のみで済むことや必要なPDBのみ起動することができるのでリソース使用量の削減が可能です。ただし、OSの再起動時にはPDBは自動的に起動する設定になっていないので自動起動設定を行うためには以下で説明する設定などが必要になります。Oracle12Cのバージョンによって設定方法が異なりますのでバージョンごとの設定方法を以下で説明いたします。

Oracle12.1.0.2での設定方法

Oracle12.1.0.2以降では以下の設定でサーバーやPC起動時にPDB自動起動することが可能です。

1. sysdbaでsqlplusにログイン
sqlplus ユーザ名/PASS as sysdba;
2. 設定を行うPDBに接続先を変更
 alter session set container = PDB名;

接続するPDBを変更します。

 show con_name;

※show con_nameコマンドで接続先のPDBを確認することができます。

3.PDBを開く
 alter pluggable database PDB名 open;

PDBを起動します。

4.PDB自動起動設定

3の設定でPDBが起動されている状態で以下のコマンドを実行します。

 alter pluggable database PDB名 save state;

上記の設定を行うことにより、サーバーやPCの起動時にPDBが起動されるようになります。


Oracle12.1.0.1での設定方法

Oracle12.1.0.1では自動起動する設定を行うことができないため、 以下の方法でサーバーやPCの起動時にタスクスケジューラーのトリガーをシステム起動時にバッチを実行することで対応を行いました。

1.PDBを起動するコマンドを作成
 alter pluggable database PDB名 open;
 quit;
2.起動バッチ作成

1で作成したSQLを呼び出すバッチを作成します。

 sqlplus ユーザ名/PASS as sysdba @c:\oracle\PDBOPEN.sql
3.タスクスケジューラーの設定

タスクスケジューラーから(2)で作成したバッチファイルをシステム起動時に開始する設定を行います。 f:id:tac7:20151205135815p:plain
※「操作」タブで新規タクスを作成します。

f:id:tac7:20151205135553p:plain

f:id:tac7:20151205135728p:plain

※トリガーの編集でバッチ起動の時間をシステム起動時間から遅らせることができる遅延時間を設定することができます。システムの起動時にPDBの起動を行うとOracleDBが起動していない状態でPDBの起動処理を実行してしまうことがあります。遅延時間に数分を設定することによりOracleDB起動後にPDBの起動処理を行うことができます。もし、上記の設定でPDBが起動しないときは遅延時間を調整してみてください。


まとめ

Oracle12Cで新しく登場したPDBですが、PCやサーバーの再起動時に自動で起動するためには、ご使用のバージョンによって今回説明させていただいた内容を参考にしていただければと思います。

ネットワークの基礎(全4回) 第1回 ネットワークの環境及び接続形態

はじめまして、ゴルフ場基幹システム開発担当している茨城出身のかたちんです。
書かせて頂く内容としては、インフラ及びセキュリティ関連になります。
初回はネットワークにおける環境や接続形態を書きます。

目次


  • ネットワークの基本環境
  • ネットワークの接続形態
    • スター型(ツリー型)
    • バス型
    • リング型

ネットワークの基本環境


企業や自宅などで利用されるそれぞれのネットワーク環境を説明します。

スタンドアローン

コンピュータを単体で利用している状態で、昔はファイルをFDでやり取りしたり、スイッチ切替て印刷したり面倒な作業を行っていました。
現在ではあまり利用するケースが少なくなってきましたが、外部と直接接続がされていない為、セキュリティ面を考慮した利用ケースが存在します。
f:id:katachin7788:20151130124040j:plain

LAN(Local Area Network)

同じ施設内またはフロア内の限定された狭い範囲内でのネットワークで、コンピュータやプリンタなどの資源をLANケーブルや無線と接続して共有できる環境です。
LANケーブルを集積するHUBやスイッチなどの集線装置を利用します。
HUBやスイッチの違いについては次回以降で説明致します。
f:id:katachin7788:20151130124132j:plain

WAN(Wide Area Network)

遠隔地にあるLAN環境同士を結ぶネットワーク環境で、専用回線や電話回線などを利用して接続します。
LANとWANの違いとしては、通信事業者が提供するネットワークサーブスを利用するかになります。
f:id:katachin7788:20151130124141j:plain

インターネット

世界中の不特定多数の人たちが接続して利用することができるネットワーク環境で、主にホームページの閲覧やメールのやり取りで使われます。 インターネット環境もWAN環境同様、LAN環境から外部に接続して資源を共有することが出来るという点では共通していますが、それを利用するための約束事に違いがあります。
WANは限定された環境に対してのみ利用することしかできませんが、一方のインターネットは公開された場所であれば、誰でも利用することが可能です。
f:id:katachin7788:20151130124148j:plain

DMZ(DeMilitarized Zone)

武装帯とも言われ、インターネットなどの外部環境に公開するための環境です。
外部公開用のコンピュータを内部に設置した場合、内部ネットワークに接続されたコンピュータへの不正なアクセスなどの危険があるため、 中間にファイアウォールを設置し、外部からはDMZ環境にしか接続はできないようにアクセス制御を行います。
f:id:katachin7788:20151130124155j:plain

ネットワークの接続形態


ネットワークを構築する際の各コンピュータなどとの接続内容についてです。

スター型(ツリー型)

HUBやスイッチなどの中継装置を中央に配置し、 各コンピュータやプリンタなどにLANケーブルを接続して利用するネットワーク接続形態。
f:id:katachin7788:20151203205506j:plain

バス型

1本の同軸ケーブルを利用するネットワーク接続形態で、両端に終端装置(ターミネータ)を設置します。
各中継アダプタ(トランシーバ)を設置し、各コンピュータやHUB(スイッチなど)に接続します。
f:id:katachin7788:20151203205512j:plain

リング型

リング状のネットワークに各コンピュータなどを接続した形態で、トークリングや光ファイバを利用したFDDIがあります。
リング状の中をトークンと呼ばれる箱のようなものが巡回し、送受信するデータがあればトークンに詰めて運びます。
論理的にみるとリング状に見えますが、物理的にみるとスター型のように中央に集線装置(コンセントレータ)を設置し、各コンピュータに接続しています。
f:id:katachin7788:20151203205516j:plain

まとめ


自分自身、独学でネットワークやらセキュリティなどを勉強し、 買った本が大雑把に説明され、最初は何を言っているのかさっぱりわからず何度も挫折しそうになりましたが、 折角やり始めたことなので諦めずに色々な書物を読み漁りました。
なのでなるべくわかり易く心掛けて今後も書いていきますので宜しくお願い致します。

VisualStudioチームエクスプローラーでのGit設定

井上です。
Visual StudioでのGit設定についての備忘録です。
この手のものは最初しかやらないだけに、次回やるときに忘れていることが多いので。

環境

GitBucket 2.6
Visual Studio Pro 2013

自身で作成したソリューションをリモートリポジトリに紐付ける

GitBucketの設定

リポジトリを普通に作るのみです*1

f:id:ihisa:20151210171334p:plain

Visual Studioの設定
  1. 対象ソリューションをVSで開く
  2. ソリューション右クリックでソリューションをソース管理に追加→gitを選ぶ*2
    f:id:ihisa:20151210161336p:plain
  3. ローカルリポジトリを作らされるので作る*3
    f:id:ihisa:20151210200233p:plain
  4. ロカールリポジトリにコミットする
    f:id:ihisa:20151210173744p:plain
  5. そのまま同期を押下し、リモートリポジトリに発行する。ここでGitBucketの対象リポジトリのHTTP Clone URLを入れる。
    f:id:ihisa:20151210173810p:plain

既に作成済みのソリューションをGitリポジトリから取得する

新規作成
  1. チームエクスプローラのローカルGitリポジトリから複製を選択
  2. GitBucketの対象リポジトリのHTTP Clone URLおよびローカルリポジトリディレクトリを入力し複製押下
    f:id:ihisa:20151210172109p:plain
  3. 作成完了後、ローカルGitリポジトリから作成したリポジトリを選択 f:id:ihisa:20151210201024p:plain
  4. チームエクスプローラーのホームアイコンを押下
  5. ソリューションに複製したソリューションがでている事を確認。これをダブルクリックすればソリューションが開く。 f:id:ihisa:20151210172334p:plain
ブランチを取得
  1. チームエクスプローラーのホームアイコンを押下
  2. 分岐を押下
  3. 新しい分岐を押下するとドロップダウンでブランチが表示されるので取得したいブランチを選択
    f:id:ihisa:20151210172452p:plain
  4. 分岐の作成を押下
    f:id:ihisa:20151210172513p:plain
  5. 発行された分岐を選択すればそのブランチに切り替わる
    f:id:ihisa:20151210172551p:plain

*1:initialize this repository with a READMEにチェックを入れてリポジトリを作成すると、Visual Studioから[ローカル分岐masterをリモートリポジトリoriginに発行できません。同じ名前の分岐が既に存在します。ローカル分岐の名前を変更してもう一度やり直してください]とメッセージ表示され同期できませんので注意

*2:既に別ソリューションをgit連携していたり、Visual Studio Onlineの設定が無い場合は無条件でgitになるようです

*3:既に別ソリューションでgit連携していたりすると、自動的にローカルリポジトリができるようです(実際になりました)

PowerShellでDynamoDBにJSONドキュメントを格納する

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

さて最近はMongoDBとお友達になりつつあったのですが、「AWS担当の」と言いつつDynamoDBを全く触っていないというのは良くないのではないかと思い始めました。皆様ご存知の通り、DynamoDBはAWSの提供するNoSQLデータベースサービスで、

  • フルマネージド:難しいことはAWSが引き受けてくれる
  • 安定性:膨大な量のサイズのデータを格納しても自動的にシャーディングが行われ(しかもオンラインで)、レイテンシは変わらない
  • JSONドキュメントをそのまま格納できる
  • 高可用性:自動的に3拠点間で多重化される

といった特徴を持つ、まさにクラウドの申し子のような代物です。また個人的には無料利用枠が割と大きめなのが重要な点で、巨大というには程遠く、手で管理するには大きすぎるJSONデータをカジュアルに管理するため、MongoDBの代わりに利用する選択肢になり得ます。今回はPowerShellでこのDynamoDBを触る方法について説明します。

f:id:nurenezumi:20151204113226j:plain

猫キャッチャー

サンプルコード

まず最初に完全な形のサンプルコードを示します。

Add-Type -Path "C:\Program Files (x86)\AWS Tools\PowerShell\AWSPowerShell\AWSSDK.Core.dll"
Add-Type -Path "C:\Program Files (x86)\AWS Tools\PowerShell\AWSPowerShell\AWSSDK.DynamoDBv2.dll"

$accessKey = "********************";
$secretAccessKey = "****************************************";
$endPoint = [Amazon.RegionEndpoint]::APNortheast1;

$client = New-Object Amazon.DynamoDBv2.AmazonDynamoDBClient($accessKey, $secretAccessKey, $endPoint);
$tableName = "YOUR-TABLE-NAME" # 自分で作ったテーブル名
$table = [Amazon.DynamoDBv2.DocumentModel.Table]::LoadTable($client, $tableName);

$document = [Amazon.DynamoDBv2.DocumentModel.Document]::FromJson('{...}') # 引数はJSON形式のテキスト
$table.PutItem($document)

前提条件は以下の通りです。

  • DynamoDBでテーブルを作成してあること
  • IAMユーザーを作成し、適切な権限が割り当ててあること
  • IAMユーザーのAPIキーを作成してあること
  • AWS Tools for Windows PowerShellがインストール済みであること
  • PowerShell 3.0以上

以下はこのコードの説明です。

DLLを直接読み込めば大抵のことはできる

上述の AWS Tools for Windows PowerShell をインストールすると、自動的に環境変数PSModulePath*1SDKのパスが追加され、特に何かを意識しなくてもGet-EC2InstanceStart-EC2InstanceといったAWSのためのコマンドレットが使えるようになります。しかしリファレンスを参照すると分かるように、現時点では項目の追加・削除・編集のためのコマンドレットは用意されていません。そのためには(上のコードのように)直接SDKの中のコードを呼び出す必要があります。

そんなときはAdd-Typeで直接DLLを読み込んでしまいましょう。

PS C:\> [Amazon.RegionEndPoint]::APNortheast1
型 [Amazon.RegionEndPoint] が見つかりません。この型を含むアセンブリが読み込まれていることを確認してください。
発生場所 行:1 文字:1
+ [Amazon.RegionEndPoint]::APNortheast1
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (Amazon.RegionEndPoint:TypeName) []、RuntimeException
    + FullyQualifiedErrorId : TypeNotFound

PS C:\> Add-Type -Path "C:\Program Files (x86)\AWS Tools\PowerShell\AWSPowerShell\AWSSDK.Core.dll"
PS C:\> [Amazon.RegionEndPoint]::APNortheast1

SystemName                                                  DisplayName
----------                                                  -----------
ap-northeast-1                                              Asia Pacific (Tokyo)

このようにアセンブリが直接ロードできて使えるようになります。

DynamoDBの準備をする

まずDynamoDBのテーブルを作成します。パーティーションキー、ソートキーを適当に設定しました。なお、このキーは1度作ったらもう変更できません(変えたければテーブルを作り直して移行する必要があります)。

f:id:nurenezumi:20151204102329p:plain

これができたら、このテーブルを触る権限を持ったIAMユーザーを準備し、APIキーを取得しておきます。

レコードを登録・更新・削除する

新規登録

まず空のテーブルへレコードを実際に登録してみます。レコードをまずPowerShell連想配列@{Key1=値1; Key2=値2; ...}で作成し、JSON形式のテキストを介してAmazon.DynamoDBv2.DocumentModel.DynamoDBEntryクラスのインスタンスに変換してからDynamoDBへ送信しています。

# 投入するデータ
$source = @(
 @{name = "Tama"; timestamp = "2015-12-04T00:00:00.000Z"; cuteness = 90;  mood = "暇"},
 @{name = "Tama"; timestamp = "2015-12-04T01:00:00.000Z"; cuteness = 100; mood = "とても暇"},
 @{name = "Tama"; timestamp = "2015-12-04T02:00:00.000Z"; cuteness = "INF"; mood = "睡眠"},
 @{name = "Wak";  timestamp = "2015-12-04T00:00:00.000Z"; cuteness = 0;   mood = "腹減った"; memo = "ラーメン" })

$source | % {
  $json = $_ | ConvertTo-Json -Compress
  $document = [Amazon.DynamoDBv2.DocumentModel.Document]::FromJson($json)
  $table.PutItem($document)
}

登録結果はManagement Consoleから確認することができます。cutenessの型は数値だったり文字列だったり、memoはあったりなかったりしますが、それでもDynamoDBは柔軟に受け入れてくれています。

f:id:nurenezumi:20151204104059p:plain

更新する

テーブルを作るときに決めたキー(ここではnametimestamp)が両方とも完全に一致するレコードを登録すると更新になります。つまりPutItemSQLのINSERT文ではなくMERGE/UPSERT文に相当するわけです。したがって次のコードはデータ部分以外は上と同じです。memo入れ子にした連想配列をセットしてみました。

$source = @(
 @{name = "Tama"; timestamp = "2015-12-04T02:00:00.000Z"; cuteness = "INF"; mood = "睡眠"; memo = "あおむけ"}, # 更新
 @{name = "Tama"; timestamp = "2015-12-04T03:00:00.000Z"; cuteness = 100; mood = "ごはん"; memo = @{menu = "高級かりかり"; ammount = 2 }})

$source | % {
  $json = $_ | ConvertTo-Json -Compress
  $document = [Amazon.DynamoDBv2.DocumentModel.Document]::FromJson($json)
  $table.PutItem($document)
}
f:id:nurenezumi:20151204134009p:plain

1行は更新、1行は新規追加になりました。memoカラムにはJSONが生で書いてあるように見えますが、よく見ると"N""S"といった見覚えのないキーが出てきています。これはDynamoDBが自動的に与えた型情報です。こちらもきちんと入れ子構造を持ったドキュメントとして管理されている証だと思ってください。

削除

ついでに削除も試しておきます。こちらも構文はまったく同じで、とにかくキーを指定してレコードを特定できればそれが処理対象になると考えればいいです。

$removed = @{name = "Wak"; timestamp = "2015-12-04T00:00:00.000Z"} | ConvertTo-JSON -Compress
$document = [Amazon.DynamoDBv2.DocumentModel.Document]::FromJson($removed)
$table.DeleteItem($document)
f:id:nurenezumi:20151204142233p:plain

レコードを検索する

次は検索を行います。検索に相当するコマンドレットも用意されていないので、やはりこちらもSDKを直接呼び出します。たとえば SELECT * FROM helloDynamoDB WHERE cuteness > 80 に相当するスキャン*2なら、カラム名演算子、値を順に指定して

$scanFilter = New-Object Amazon.DynamoDBv2.DocumentModel.ScanFilter
$scanFilter.AddCondition("cuteness", [Amazon.DynamoDBv2.DocumentModel.ScanOperator]::GreaterThan, 80)
$search = $table.Scan($scanFilter)

で検索ができます。この時点で得られる$searchはカーソルのようなもので、実際の結果はGetNextSet()を呼び出して取得します(カーソルとは違って1件ずつではなく、最大1MB分のデータがまとめて返されます)。

PS C:\> $search = $table.Scan($scanFilter)
PS C:\> $documentList = $search.GetNextSet()
PS C:\> $documentList

Key                                                         Value
---                                                         -----
timestamp                                                   2015-12-04T00:00:00.000Z
mood                                                        暇
name                                                        Tama
cuteness                                                    90
timestamp                                                   2015-12-04T01:00:00.000Z
mood                                                        とても暇
name                                                        Tama
cuteness                                                    100

確かに2件の結果が取得できました。

実際の用途

今回はPowerShellからDynamoDBを利用する方法について書きました。次回はAWS ConfigのログをDynamoDBに格納し、PowerShellから検索して変更履歴を出力するという(多少は)実用的な用途に使ってみたいと思います。お楽しみに!

*1:ここで指定されたパスに配置されたモジュールは自動的に読み込まれるようになります。環境変数PATHのPowerShell版だと思えばだいたい合っています

*2:インデックスを使わないテーブルスキャンのようなものだと考えてください。したがって低速ですがすべての項目が検索対象に指定できます。テーブル作成時に指定したnameを使う検索なら、「スキャン」ではなく高速な「クエリ」が利用できます