Amazon Lambdaを試してみる

# Amazon Lambdaって？
- 事前に定義したコードを実行するサービス
- 処理の実行トリガは、何かしらのイベント(S3にファイルを配置、ストリームデータを受信、など)
- 処理自体はnode.js/Javaで実装
- サーバ、ロギング、スケーリングなどのインフラ設計・管理が不要
- 実行回数・処理によって課金

細かい部分はAWSオフィシャル資料に任せます。
[AWS Black Belt Techシリーズ AWS Lambda](http://www.slideshare.net/AmazonWebServicesJapan/aws-black-belt-tech-aws-lambda)

# サンプルシナリオ
CloudTrailのログがS3に置かれたら、ログ内容を確認してNG操作があればSNSで通知させてみます。

![](http://docs.aws.amazon.com/ja_jp/lambda/latest/dg/images/wt-cloudtrail-100.png)


マニュアルにあるウォークスルーをベースにしております。
[AWS Lambda Walkthrough 5: Handling AWS CloudTrail Events Using the AWS CLI (Node.js) - AWS Lambda](http://docs.aws.amazon.com/ja_jp/lambda/latest/dg/wt-cloudtrail-events-adminuser.html)


# 環境構築
## 事前準備
- S3バケットの作成、CloudTrailロギング設定

S3バケットを作成し、CloudTrailを設定します。
(詳細は割愛します)

- SNSトピック用意

メール通知を行うSNS Topicを作成し、送信先メールアドレスをSubscription設定しておきます。
(詳細は割愛します)

- テスト用CloudTrailログ配置

以下のJSONファイルを作成し、S3バケットの直下に置いておきます。

```json:ExampleCloudTrailLog.json
{
   "Records":[
      {
         "eventVersion":"1.02",
         "userIdentity":{
            "type":"Root",
            "principalId":"account-id",
            "arn":"arn:aws:iam::account-id:root",
            "accountId":"account-id",
            "accessKeyId":"access-key-id",
            "sessionContext":{
               "attributes":{
                  "mfaAuthenticated":"false",
                  "creationDate":"2015-01-24T22:41:54Z"
               }
            }
         },
         "eventTime":"2015-01-24T23:26:50Z",
         "eventSource":"sns.amazonaws.com",
         "eventName":"CreateTopic",
         "awsRegion":"ap-northeast-1",
         "sourceIPAddress":"205.251.233.176",
         "userAgent":"console.amazonaws.com",
         "requestParameters":{
            "name":"dropmeplease"
         },
         "responseElements":{
            "topicArn":"arn:aws:sns:ap-northeast-1:account-id:exampletopic"
         },
         "requestID":"3fdb7834-9079-557e-8ef2-350abc03536b",
         "eventID":"17b46459-dada-4278-b8e2-5a4ca9ff1a9c",
         "eventType":"AwsApiCall",
         "recipientAccountId":"account-id"
      },
      {
         "eventVersion":"1.02",
         "userIdentity":{
            "type":"Root",
            "principalId":"account-id",
            "arn":"arn:aws:iam::account-id:root",
            "accountId":"account-id",
            "accessKeyId":"access-key-id",
            "sessionContext":{
               "attributes":{
                  "mfaAuthenticated":"false",
                  "creationDate":"2015-01-24T22:41:54Z"
               }
            }
         },
         "eventTime":"2015-01-24T23:27:02Z",
         "eventSource":"sns.amazonaws.com",
         "eventName":"GetTopicAttributes",
         "awsRegion":"ap-northeast-1",
         "sourceIPAddress":"205.251.233.176",
         "userAgent":"console.amazonaws.com",
         "requestParameters":{
            "topicArn":"arn:aws:sns:ap-northeast-1:account-id:exampletopic"
         },
         "responseElements":null,
         "requestID":"4a0388f7-a0af-5df9-9587-c5c98c29cbec",
         "eventID":"ec5bb073-8fa1-4d45-b03c-f07b9fc9ea18",
         "eventType":"AwsApiCall",
         "recipientAccountId":"account-id"
      }
   ]
}
```

- IAMロールの用意

Lambdaが実行された時にS3へのアクセス、SNSのPublish、CloudWatchLogsへのロギングが必要になるので、それらの権限を付与したロールを作成します。

```json:lambda_cloudtrail_snspublish
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "logs:*"
      ],
      "Resource": "arn:aws:logs:*:*:*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject"
      ],
      "Resource": "arn:aws:s3:::*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "sns:Publish"
      ],
      "Resource": "<SNS TopicのARNを設定>"
    }
  ]
}
```

- ローカル環境にNode.jsの実行環境を用意

詳細は専門ブログ等に任せるとして、Node.jsの実行環境を用意しておきます。
私はこの辺りを参考に環境を準備しました。

[nvm を利用したnode.js のインストール - もろず blog](http://moro-archive.hatenablog.com/entry/2014/03/01/213314)


## Lambda Functionの実装
- ローカル環境でJSを実装

ウォークスルーを読みながら以下のコードを作成。

```javascript:CloudTrail_to_SNS.js
var aws  = require('aws-sdk');
var zlib = require('zlib');
var async = require('async');

var EVENT_SOURCE_TO_TRACK = /sns.amazonaws.com/;
var EVENT_NAME_TO_TRACK   = /CreateTopic/;
var DEFAULT_SNS_REGION  = 'ap-northeast-1';
var SNS_TOPIC_ARN       = '<SNS TopicのARNを設定>';

var s3 = new aws.S3();
var sns = new aws.SNS({
    apiVersion: '2010-03-31',
    region: DEFAULT_SNS_REGION
});

exports.handler = function(event, context) {
    var srcBucket = event.Records[0].s3.bucket.name;
    var srcKey = event.Records[0].s3.object.key;

    async.waterfall([
        function fetchLogFromS3(next){
            console.log('Fetching compressed log from S3...');
            s3.getObject({
               Bucket: srcBucket,
               Key: srcKey
            },
            next);
        },
        function uncompressLog(response, next){
            console.log("Uncompressing log...");
            zlib.gunzip(response.Body, next);
        },
        function publishNotifications(jsonBuffer, next) {
            console.log('Filtering log...');
            var json = jsonBuffer.toString();
            console.log('CloudTrail JSON from S3:', json);
            var records;
            try {
                records = JSON.parse(json);
            } catch (err) {
                next('Unable to parse CloudTrail JSON: ' + err);
                return;
            }
            var matchingRecords = records
                .Records
                .filter(function(record) {
                    return record.eventSource.match(EVENT_SOURCE_TO_TRACK)
                        && record.eventName.match(EVENT_NAME_TO_TRACK);
                });

            console.log('Publishing ' + matchingRecords.length + ' notification(s) in parallel...');
            async.each(
                matchingRecords,
                function(record, publishComplete) {
                    console.log('Publishing notification: ', record);
                    sns.publish({
                        Message:
                            'Alert... SNS topic created: \n TopicARN=' + record.responseElements.topicArn + '\n\n' +
                            JSON.stringify(record),
                        TopicArn: SNS_TOPIC_ARN
                    }, publishComplete);
                },
                next
            );
        }
    ], function (err) {
        if (err) {
            console.error('Failed to publish notifications: ', err);
        } else {
            console.log('Successfully published all notifications.');
        }
        context.done(err);
    });
};
```

- ライブラリのインストール

jsの先頭でrequireしているライブラリのうち、Lambdaに準備されてないものをローカル環境に入れておきます。

```shell
$ npm install async
```

恐らく自分のホームディレクトリ配下に配置されているので、作業ディレクトリに対してシンボリックリンクを貼っておきます。

```shell
$ ln -s ~/node_modules ./node_modules
```

## パッケージング
最上位にフォルダが入らないようにzipで固めます。

```shell
$ zip -r CloudTrail_to_SNS.zip CloudTrail_to_SNS.js node_modules
```

## アップロード・実行
- アップロード

Management ConsoleからLambda Functionを作成します。

![スクリーンショット 2015-07-09 23.04.02.png](https://qiita-image-store.s3.amazonaws.com/0/64811/43a89c4b-3c5d-e172-6f8c-fdeaf587f25b.png)


- テスト用のS3イベントを用いて手動実行

Sample Eventに以下を入力し、Invokeしてみます。

```json:input_sample_event
{
  "Records": [
    {
      "eventVersion": "2.0",
      "eventSource": "aws:s3",
      "awsRegion": "ap-northeast-1",
      "eventTime": "1970-01-01T00:00:00.000Z",
      "eventName": "ObjectCreated:Put",
      "userIdentity": {
        "principalId": "EXAMPLE"
      },
      "requestParameters": {
        "sourceIPAddress": "127.0.0.1"
      },
      "responseElements": {
        "x-amz-request-id": "C3D13FE58DE4C810",
        "x-amz-id-2": "FMyUVURIY8/IgAtTv8xRjskZQpcIZ9KG4V5Wp6S7S/JRWeUWerMUE5JgHvANOjpD"
      },
      "s3": {
        "s3SchemaVersion": "1.0",
        "configurationId": "testConfigRule",
        "bucket": {
          "name": "audit-cloudtrail-mochiko",
          "ownerIdentity": {
            "principalId": "EXAMPLE"
          },
          "arn": "arn:aws:s3:::<S3バケット名>"
        },
        "object": {
          "key": "ExampleCloudTrailLog.json.gz",
          "size": 1024,
          "eTag": "d41d8cd98f00b204e9800998ecf8427e"
        }
      }
    }
  ]
}
```

![スクリーンショット 2015-07-10 3.05.47.png](https://qiita-image-store.s3.amazonaws.com/0/64811/1eb4aa09-4e7c-1427-9d03-9eebda5afc21.png)!

いい感じにメールが来ました。

## S3をLambdaのイベントソースに設定

S3にファイルが置かれたらLambdaへイベント通知が飛ぶようにします。

![スクリーンショット_2015-07-09_23_31_45_のコピー.png](https://qiita-image-store.s3.amazonaws.com/0/64811/f536a886-4c2f-ee92-2ecf-e8a75867832b.png)


## 実際に動かしてみる

今回のサンプルだと、SNS Topicを新規作成(CreateTopic)したことを検知するようになっているので、実際にTopicを作成してみます。

![スクリーンショット 2015-07-09 23.33.48.png](https://qiita-image-store.s3.amazonaws.com/0/64811/c8bead8d-5e4c-40ef-18a0-0e9c06d0340c.png)

![スクリーンショット_2015-07-10_3_18_22_2.png](https://qiita-image-store.s3.amazonaws.com/0/64811/3a59b16b-9e7a-8102-80df-bd0b9dd0b885.png)

上手く通知がきました。


# 所感
- Lambda Functionの実装はWeb Consoleからもできるが、ローカルに開発環境を用意するほうがベター。

ライブラリを呼ばないぐらいシンプルな場合はWebConsoleでも十分。
今回はMac上にNode.jsの環境を作ってローカルで実装、パッケージングし、Webへアップロードしました。


- Shellやプログラムの実行も可能

インフラエンジニア目線だとJavaScriptよりShellScriptのほうが慣れているかと。
使いどころは今後検証してみたいと思います。

- 「シンプルな処理 & 何度も繰り返す処理」には適用しやすい

リアルタイムに決められた定型処理をやらせるには持ってこいなサービスな感じを受けました。
一回の実行単価が非常に安いため、上手く使えばEC2要らずで構成を組めそうですね。

# 注意点
- VPC内リソースにアクセスができないため、利用シーンの検討は必要。

Enterprise向けだとVPCでセキュリティをガチガチに固めてたりする使われ方が多いと思うので、今回のようなサービス間通信が必要なケースは上手く当てはめられないかと思われます。
これに気づかずに別ユースケースを実装しようとした際にハマりました…

> Q: 自分の AWS Lambda 関数を使用して、Amazon VPC の内側にあるリソースにアクセスでき> ますか?
> AWS Lambda 関数は現時点では、VPC の内側にあるリソースにアクセスできません。
> [よくある質問 - AWS Lambda (コードの実行、リソース自動管理プラットフォーム) | アマゾン ウェブ サービス（AWS 日本語）](http://aws.amazon.com/jp/lambda/faqs/)

- 処理時間は最長でも60秒まで

Lambdaは最長60秒までしか処理をしてくれません。
Redshiftへ大量にデータを入れるなどの待機処理が加わる使い方には向いてないかもしれません。

# 参考情報
## イベントトリガの種類
- S3バケットのオブジェクト操作
- SNS通知
- Kinesis Stream
- DynamoDB Stream
- LambdaのAPI(Invokeコマンド)
- Cognito(モバイル端末からのイベント呼び出し)