AWS Config Rule(Preview)を使って定期的にEC2の起動数を確認する

2015年のre:Inventで紹介されたAWS Config Ruleのプレビューを利用できるようになったので、内容の確認とLambda連携で独自のルールを定義して、EC2起動数を確認してみたのでメモ。

# 参考

+ [【AWS発表】AWS Config Rules - Dynamic Compliance Checking for Cloud Resources](http://aws.typepad.com/aws_japan/2015/10/aws-config-rules-dynamic-compliance-checking-for-cloud-resources.html)
+ [AWS Configの拡張機能「AWS Config Rules」の要素を確認してみた #reinvent](http://dev.classmethod.jp/cloud/aws/aws-config-rules-details/)
+ [AWS Config Rulesのカスタムルール(Lambda連携)を触ってみた #reinvent](http://dev.classmethod.jp/cloud/aws/aws-config-custom-rules/)

# 結論

+ AWS Config Ruleを使えばAWSリソースの定期的なチェックができる
+ ルールのチェックは定期実行(1時間ごとなど)とリソースの作成、変更時にイベント駆動で実施する方式がある
+ 既にAWSが用意したルールであればコーディングなしで、パラメーターを設定するだけで利用できる
+ AWSが用意したルールがない場合もLambdaと連携し、独自ルールを設定することができる

# AWS Config Ruleとは?

EC2やSecurityGroupなどのAWSのリソースの構成を定期的もしくは作成・変更などを契機にルールに合致しているか確認できるようになったというものです。

AWS側で最初から設定できるルール（マネージドルール）は以下のようなものがあります。

+ 特定のタグ(およびキー)が設定されているか
+ SecutiryGroupの22ポートが空いていないか

その他、詳細は公式ドキュメントで確認できます。

また、Lambdaと連携することで独自のルールを作成することができます。

+ 起動したEC2のインスタイプがt2.microか
+ 起動しているEC2の総数が規定数より多くないか

など。ルールをLambdaで独自に作れるのでどんなルールでも作成できます。

それLambdaのcron実行などでもできるじゃんという話もあるかもしれませんが、AWS Configを使うことで設定値をパラメーターとして設定できるのが良いと思いました。例えば上記だと独自ルールの部分「t2.micro」というのをパラメーター化することでインスタンスタイプを変える場合にもコードを変更せず、AWS Config Ruleのパラメーターを変えるだけで設定できるというのは大きな利点だと思いました。(そのうちLambdaでもパラメーターのような物が使えるようになるかもしれませんが）

# 定期的にEC2の起動数を確認するカスタムルールを試す

実際に使ってみて内容を確認してみます。
公式ドキュメントの以下を参考に定期的にEC2の合計起動数を確認するルールを試します。

[Example AWS Lambda Functions for AWS Config Rules (Node.js)](https://docs.aws.amazon.com/ja_jp/config/latest/developerguide/evaluate-config_develop-rules_nodejs-sample.html)

## Lambda Functionの作成


まず、以下よりサンプルをダウンロードをし、解答、ファイル名の変更などを行います。

[samples/InstanceTypeCheck_ConfigRuleSample.zip](https://docs.aws.amazon.com/ja_jp/config/latest/developerguide/samples/InstanceTypeCheck_ConfigRuleSample.zip)

```
$wget https://docs.aws.amazon.com/ja_jp/config/latest/developerguide/samples/InstanceTypeCheck_ConfigRuleSample.zip
$unzip InstanceTypeCheck_ConfigRuleSample.zip
$rm InstanceTypeCheck_ConfigRuleSample.zip
$mv InstanceTypeCheck.js InstanceSumCheck.js
$vi InstanceSumCheck.js
```

公式ドキュメントにあるように以下を利用します。

```InstanceSumCheck.js
var aws  = require('aws-sdk');
var s3 = new aws.S3();
var zlib = require('zlib');
// Include the Node.js SDK with AWS Config APIs explicitly. Temporarily necessary while AWS Config APIs are being released.
var config = new aws.Service({
    apiConfig: require('./config-2014-11-12.normal.json'),
    region: 'us-east-1'
});

// This function uses the collection of configuration items as well as the
// rule parameters to compute the compliance value.
// The example counts the number of resources with type equal to the value
// of the 'applicableResourceType' and compares that number to the value
// of the 'maxCount' parameter.

function evaluateCompliance(configurationItems, ruleParameters, context) {
    var applicableResourceType = ruleParameters.applicableResourceType;
    var maxCount = parseInt(ruleParameters.maxCount);
    var count = 0;
    for (var i = 0; i < configurationItems.length; i++) {
        var item = configurationItems[i];
        if (item.resourceType === applicableResourceType) {
            count++;
        }
    }
    if (count > maxCount) {
        return 'NON_COMPLIANT';
    } else {
        return 'COMPLIANT';
    }
}

// This is the handler that's invoked by Lambda
// Most of this code is boilerplate; use as is

exports.handler = function(event, context) {
    var invokingEvent = JSON.parse(event.invokingEvent);
    var ruleParameters = JSON.parse(event.ruleParameters);
    var s3key = invokingEvent.s3ObjectKey;
    var s3bucket = invokingEvent.s3Bucket;
    var accountId = getAccountId(invokingEvent);
    var orderingTimestamp = invokingEvent.notificationCreationTime;
    readSnapshot(s3, s3key, s3bucket, function(err, snapshot) {
        if (err === null) {
            var evaluation = {
                ComplianceResourceType: 'AWS::::Account',
                ComplianceResourceId: accountId,
                ComplianceType: evaluateCompliance(snapshot.configurationItems, ruleParameters, context),
                OrderingTimestamp: orderingTimestamp
            };
            var putEvaluationsRequest = {
               Evaluations: [
                   evaluation
               ],
               ResultToken: event.resultToken
            };
            config.putEvaluations(putEvaluationsRequest, function (err, data) {
                if (err) {
                    context.fail(err);
                } else {
                    context.succeed(data);
                }
            });
        } else {
            context.fail(err);
        }
    });
};

// Extract the account ID from the event

function getAccountId(invokingEvent) {
    var accountIdPattern = /AWSLogs\/(\d+)\/Config/;
    return accountIdPattern.exec(invokingEvent.s3ObjectKey)[1];
}

// Reads and parses the ConfigurationSnapshot from the S3 bucket
// where Config is set up to deliver
function readSnapshot(s3client, s3key, s3bucket, callback) {
    var params = {
        Key: s3key,
        Bucket: s3bucket
    };
    var buffer = "";
    try {
        s3client.getObject(params)
            .createReadStream()
            .pipe(zlib.createGunzip())
                .on('data', function(chunk) {
                    buffer = buffer + chunk;
                })
                .on('end', function() {
                    callback(null, JSON.parse(buffer));
                });
    } catch(err) {
        callback(err, null);
    }
}
```

+ 現在はConfig RuleがPreviewでSDKに対応してないため、config-2014-11-12.normal.jsonという設定を利用しており、zip時に含める必要がある。
+ ルールに合致するかの評価は **evaluateComplianceメソッド**で実行している。ここを変更することで定期処理時のルールを独自に設定することができる
+ 定期的にAWS Config Ruleを利用する場合には取得できるS3にアップロードされるAWS Configのスナップショット(AWS リソース情報が定義されているもの)を利用している
+ applicableResourceTypeというパラメーターでどのAWSリソースの数を数えるか定義する。また、maxCountパラメーターを対象のAWSリソースの上限値とし、その値を超えた場合にルール違反とする。上記いずれのパラメーターもAWS Config Ruleで値の設定ができるのでコード上でハードコーディングしなくても良い。


ZIP圧縮します。

```bash
$zip -r InstanceSumCheck.zip InstanceSumCheck.js config-2014-11-12.normal.json
```

準備ができたのでLambda Functionを作成します。

+ Create Lambda Function
+ Blue Printの画面（テンプレを選ぶ画面）はSkip

以下を設定。

+ Name->InstanceSumCheck
+ Runtime->Node.js
+ Code entry type->ZIPファイルのアップロード
+ Handler->InstanceSumCheck.handler
+ Role->lambda_config_periodic_execution.以下のようなルールを設定。AWS Config Ruleとの連携のためにconfig:PutEvaluationsを付与する。また、Lambdaの処理でS3から情報を取得するため、s3:GetObjectについても付与。

```
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents",
        "config:PutEvaluations",
        "s3:GetObject"
      ],
      "Resource": "*"
    }
  ]
}
```

AWS CLIでLambda FunctionにAWS Configの権限を追加します。

`--function-name`の部分は作成したLambdaFuncionのARNを設定し、`--statement-id`には任意のIDを設定します。

```bash
$aws lambda add-permission --function-name arn:aws:lambda:us-east-1:xxxxxx:function:InstanceSumCheck --statement-id "AddConfigPermission" --action lambda:InvokeFunction --principal config.amazonaws.com --region us-east-1
```

## カスタムルール

ではAdd custom ruleボタンでカスタムルールを作成しましょう!

+ Name->instance-sum-check
+ ARN->先ほど作成したLambdaFunctionのARN
+ Trigger->Periodic
+ Frequency->1hr
+ Rule parameters->Key「maxCount」,Value「2」 Key「applicableResourceType」, Value「AWS::EC2::Instance」

## 確認する

インスタンスがない状態で設定すると問題がないので **Compliant** となっています。

![Screen Shot 2015-11-01 at 7.12.18 PM.png](https://qiita-image-store.s3.amazonaws.com/0/20193/0db1b92a-0d52-49af-8812-3083de3e63b1.png "Screen Shot 2015-11-01 at 7.12.18 PM.png")

その後、インスタンスを3つ以上起動してみます。すると以下のようにルールで **noncompliant** となりました。

![Screen Shot 2015-11-02 at 8.32.31 AM.png](https://qiita-image-store.s3.amazonaws.com/0/20193/434ba349-8882-cb5b-c25d-ee076cf5bfca.png "Screen Shot 2015-11-02 at 8.32.31 AM.png")


このようにルールを定義しておくことで企業で定めた設定などが守られているかをAWS Config Ruleにて守ることができ、とても便利そうです!