Mackerel  AWS Lambda agentを作った

# 何を作ったか

MackerelでAWS Lambdaのログ監視をしたいという時に、EC2などのインスタンスが必要になってしまうのがどうしても気になって・・・。
**サーバーレスで動くシステムの監視にサーバーが必要**になるのは何か悲し気持ちになるは私だけなのかな？皆さんはどうでしょうか？
今回そんな思いを胸に2年ほど前から構想を温めつつ少しづつ書いていたサーバーレス(AWS Lambda)で動作するMackerel-agentをなんとか動かせるようにしたのでそれについてグタグダ書いてみました。

# ソースコード

https://github.com/masahide/mackerel-awslambda-agent/
とりあえずチェック系のプラグイン(check-aws-cloudwatch-logs-insights等)がなんとか動く所までを作ったという程度で、ぜんぜんドキュメントなども整ってなくお粗末で申し訳ない状態ですが、これから暇を見て色々整えつつVPC lambda化してメトリック系プラグインを動かしてRDS,elasticacheなどの監視ができるようにしたいと思ってます。

# 全体構成
以下のように複数のLambda関数に分けた構造にしています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26566/32908fa1-df37-a088-4b9e-7a90cb2c67d3.png)
### 処理の流れ:
大まかな処理の流れは以下の通りで、CloudwatchEventのcronで毎分起動実行されます

1. s3に設置している`mackerel config`を`invoker(lambda関数)`が読む
2. コンフィグに従い`invoker`が`checker(lambda関数)`を非同期invoke
3. `checker`に同梱したプラグイン実行ファイルを起動
4. プラグインのレポートをSQSの`Report Queue`にポスト
5. `sender(lambda関数)`が`Report Queue`に溜まったレポートをまとめて`Mackerel`にポスト


# 工夫した点など

### プラグインが利用するステートファイルの永続化

今回のシステムでは`checker lambda関数`に必要なプラグインの実行ファイルを同梱し、[lambda関数内からプロセス起動する形](https://github.com/masahide/mackerel-awslambda-agent/blob/13d167de03b291503258f01f303586e5a196bf77/pkg/checkplugin/checkplugin.go#L134-L144)で実装しています。
その別プロセスとして起動するプラグインには大抵の場合、次回実行時に備えて状態を[StateDir](https://github.com/mackerelio/go-check-plugins/blob/0fb542a10f72eb44e7948bae724f3af43b609edf/check-aws-cloudwatch-logs/lib/check-aws-cloudwatch-logs.go#L32)のディレクトリに保存する仕組みがあります。しかし、lambda関数の場合は、実行が終了してしまうと一時保存したファイルなどは次回の実行時まで維持される保証がありませんので、これらのファイルを関数が終了する前にどこか別の永続領域に退避する必要があります。
今回は、プラグインが実行終了したタイミングでそれらのステートファイルをシリアライズし[dynamodbに保存](https://github.com/masahide/mackerel-awslambda-agent/blob/13d167de03b291503258f01f303586e5a196bf77/pkg/state/state.go#L120-L141)するようにしました。（上記の図には見づらくなるので載せませんでしたが...)

### ステートファイルパスの固定

mackerel-agentには、[AWS_SECRET_ACCESS_KEY環境変数などに設定されている値を元にstateファイルパスを算出する実装](https://github.com/mackerelio/go-check-plugins/blob/0fb542a10f72eb44e7948bae724f3af43b609edf/check-aws-cloudwatch-logs/lib/check-aws-cloudwatch-logs.go#L73-L82)があります。この実装には、同じプラグインを使う設定が複数個存在する場合でもそれらが利用するステートファイルが重複しないようにする重要な役目があるのですが、`lambda`の場合は、AWS_SECRET_ACCESS_KEY環境変数には制限時間のある一時キーが設定されているため、この実装が原因でファイルパスが実行時毎に異なってしまいうまくステートファイルが読めなくなってしまいます。
そのため今回は、AWS_SECRET_ACCESS_KEY環境変数などに設定されているAWSクレデンシャル情報を全て[~/.aws/credentials](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-configure-files.html#cli-configure-files-where)ファイルに保存し直し、[AWS_SECRET_ACCESS_KEY環境変数をunsetする](https://github.com/masahide/mackerel-awslambda-agent/blob/13d167de03b291503258f01f303586e5a196bf77/pkg/awsenv/awsenv.go#L70-L89)ことで生成されるstateファイルのパスを固定化されるようにしました。


### Mackerelのメンテナンスに備える再試行処理

Mackerel自体がメンテナンスで数時間サービスが止まることがあるのですが、その間でも監視を続行して欲しい（通知は受けれなくても記録は後でみたい）ので、メンテ中はプラグインの実行結果を溜めておいてメンテ終わった後にまとめてポストするようにします。今回はSQSを使ってこの処理を実現しました。SQSのキューで計測処理と送信処理を分離しているので、mackerelがメンテになって投稿処理がつまっても、計測処理には何も影響がありません。また、SQSは簡単に再試行処理を実装できるので大変便利です。特にlambda関数のeventトリガーとしてSQSを利用した場合の再試行処理はとても簡単で、必要な処理が失敗した場合は[その関数をエラーとして返すだけの実装](https://github.com/masahide/mackerel-awslambda-agent/blob/13d167de03b291503258f01f303586e5a196bf77/cmd/sender/main.go#L38)であとはSQSが良い感じに再試行してくれるようになります。再試行のタイムアウト時間や試行回数はSQSのQueue側の設定値を調整するだけす。(これについてはCDKで行います)


### CDKでのデプロイ

複数のlambda関数やS3,dynamodb,SQS,IAM roleなど結構な数のAWSリソースを設定する必要があり、これらを手でひとつづつ登録するのは現実的ではありません。今回はこれらを[CDK](https://aws.amazon.com/jp/cdk/)でdeployするようにしました。
CDKを利用するとAWSリソースへのアクセスに必要になるIAM roleやIAM Policyを最小限の権限に絞って設定する事がとても簡単で直感的に記述できます。また、大量に作ることになるAWSリソースを1つの「アプリケーション」という括りでAWSコンソール上でも一元管理できるようになっており。迷子リソースなどが出にくいのもCDKの良いところだと思います。

# デプロイ方法

### 前提条件

ビルドとデプロイには以下のソフトウェアを利用します

* Node.js 10.3.0以降(CDKでデプロイ用)
* go 1.14以降(lambda関数のビルド用)
* GNU Make 3.81(lambda関数のビルド用)
* AWS CLI(CDKを利用するためのcredentials設定用)

CDKを利用しAWS環境にデプロイする際はAdministratorAccess権限のアカウントもしくはロールが必要になり、それらをAWS CLI等を利用し~/.aws/credentialsファイルを構成しておく必要があります。。詳しくは[CDKの前提条件のページ](https://cdkworkshop.com/15-prerequisites/200-account.html)を参照してください。


### 全体の流れ
デブロイの一連の流れは、以下の3工程です。

- エージェントの設定ファイルの準備
- lambda関数のビルド
- 上記で準備したファイルをCDKでデプロイ

### エージェントの設定ファイルの準備

エージェントの設定は、プラグインの設定ファイルとCDKの設定の2カ所です。

#### プラグインの設定
`config/agent.conf.example` にサンプルのコンフィグファイルがあります。

```bash
$ git clone https://github.com/masahide/mackerel-awslambda-agent.git
$ cd mackerel-awslambda-agent
$ cp config/agent.conf.example config/agent.conf
```
として編集して下さい。中身は以下のような内容で、大体mackerel-agentと同じです。(と言ってもAPIkeyの指定とpluginの設定しかありません)

```toml:config/agent.conf
apikey = "APIKEY"

[plugin.checks.cloudwatch-logs]
command = "check-aws-cloudwatch-logs-insights --log-group-name=/aws/lambda/xxxxxxxxx --filter='filter @message like /ERROR/'"
```
このconfig/agent.confは後述のCDKデプロイでS3バケットにアップロードされます。

#### CDKの設定
以下のcdk.jsonのcontext内にある`ORGANIZATION`と`HOSTNAME`を修正します。
* `ORGANIZATION`はmackerelのオーガニゼーション名
* `HOSTNAME` はこのawslambda-agentをmackerelに登録するホスト名

```json:cdk/cdk.json
{
  "app": "npx ts-node bin/cdk.ts",
  "context": {
    "@aws-cdk/core:enableStackNameDuplicates": "true",
    "aws-cdk:enableDiffNoFail": "true",
    "@aws-cdk/core:stackRelativeExports": "true",
    "@aws-cdk/aws-ecr-assets:dockerIgnoreSupport": true,
    "env": "dev",
    "appName": "mackerel-awslambda-agent",
    "envs": {
        "dev": {
            "environment" : {
                "HOSTNAME":     "mackerel-awslambda-agent", <-- ここ
                "ORGANIZATION": "test",   <-- ここ
                "S3KEY":        "config/agent.conf"
            }
        },
        "prd": {
            "environment" : {
                "HOSTNAME":     "mackerel-awslambda-agent", <-- ここ
                "ORGANIZATION": "test", <-- ここ
                "S3KEY":        "config/agent.conf"
            }
        }
    }
  }
}
```

[node.js](https://nodejs.org/en/download/)と[golang](https://golang.org/doc/install)のインストールはそれぞれのサイトにお任せしするとしてそれ以降を以下に記載します。

### lambda関数のビルド

lambda関数はmakeコマンドで作成します

```bash
$ make build
```
上記で3つのlambda関数がビルドされ`.dist/` 以下に配置されます。一緒に、check-aws-cloudwatch-logs-insightsプラグインの実行ファイルを[github](https://github.com/mackerelio-labs/check-aws-cloudwatch-logs-insights)から取得され`.dist/checker/`に配置されます。

### cdkによるデプロイ

#### cdkのインストール

cdk本体を入れていない場合は以下を実行しインストール

```bash
$ npm install -g aws-cdk
$ cdk --version
1.76.0 (build c207717)
```

cdkを対象のAWSアカウントに初めて利用する際は、[cdkのブートストラップスタックをインストールする必要があります](https://cdkworkshop.com/20-typescript/20-create-project/500-deploy.html)。以下のコマンドでインストールできます。

```bash
cdk bootstrap
```

####  デプロイ
```bash
# cdkディレクトリに移動
cd cdk
# 関連パッケージのインストール
npm i 
# cdk build
npm run build
#  cdk deploy
npm run cdk deploy
```
以上で完了です。


# 参考:

- https://github.com/kevinslin/open-cdk の `Tips and Best Practices`