新システムでAWSの監視を見直した話

GMOメイクショップ コアグループ エンジニアの越川です。 前職あたりから、AWS関連の仕事をさせてもらうことが多くなってきていて、 弊社の新システムの構成や構築を設計経験させていただきました。 今回は、監視について記載させていただきます。

1. 全体構成図

早速ですが、今回構築した全体図から

監視の全体イメージ

2. 監視対象

今回導入した各項目は後述いたします。

  1. ログ

  2. メトリクス

  3. 外形

  4. APM(初期導入を見送りましたが、X-RayかDatadog Application Performance Monitoringを入れたいと密かに計画しております)

3. ログ監視

現行システムはCloudWatchの料金が課題の一つとなっておりました。 そのため、今回の構成では、以下の整理を行いました。

ログルーターサイドカーを用意して、以下で分けました。 アラートが発砲したら、AthenaでS3のログを検索して調査します。

  • ログレベルがエラー以上のログ:CloudWatchLogs→CloudWatchメトリクスフィルター→CloudWatch Alarm→SNS→Chatbot→Slack
  • ログの全量:Kinesis Data Firehose→S3

具体的な設定ファイルは以下のような形で準備しました。

FROM amazon/aws-for-fluent-bit:latest
COPY ./conf/* /fluent-bit/etc/

RUN yum install -y gcc make tar curl lua-devel && \
    curl -L https://www.kyne.com.au/~mark/software/download/lua-cjson-2.1.0.tar.gz -o lua-cjson-2.1.0.tar.gz && \
    tar -xzf lua-cjson-2.1.0.tar.gz && \
    cd lua-cjson-2.1.0 && \
    make && \
    make install && \
    cd .. && \
    rm -rf lua-cjson-2.1.0 lua-cjson-2.1.0.tar.gz

RUN yum remove -y gcc make tar

RUN yum clean all

Luaを使って、出力ログを加工しているので、いくつかのライブラリをインストールしていますが、そういう用途がなければ、最初の2行で動くと思います。

[SERVICE]
    Flush 1
    Grace 30
    Parsers_File /fluent-bit/etc/parsers.conf
# ECSからのログ入力値を250MBに制限
[INPUT]
    Name forward
    unix_path /var/run/fluent.sock
    Mem_Buf_Limit 250MB
# ログ項目にcontainer_idを追加
[FILTER]
    Name lua
    Match *-firelens-*
    Script add_container_id.lua
    call add_container_id

# ログ項目のみをFilter
[FILTER]
    Name     parser
    Match    *-firelens-*
    Key_Name log
    Parser   json
# ヘルスチェックアクセスを除外
[FILTER]
    Name grep
    Match *-firelens-*
    Exclude $msg ^(?=.*healthcheck\.Check).*$
    Exclude $msg ^(?=.*grpc\.health).*$
# エラーログにタグ付け
[FILTER]
    Name     rewrite_tag
    Match    *-firelens-*
    Rule     $level (error|fatal) error-$container_id false
# CloudWatch出力
[OUTPUT]
    Name  cloudwatch_logs
    Match error-*
    region ap-northeast-1
    log_group_name  [log_group_name]
    log_stream_name [log_stream_name]
    auto_create_group false
    workers             1
    auto_retry_requests On
    retry_limit         5
    net.keepalive Off
#S3出力
[OUTPUT]
    Name   kinesis_firehose
    Match  *
    region ap-northeast-1
    delivery_stream [delivery_stream]

confファイルは大体こんな感じで用意しました。OutPutの部分で切り分けています。 こちらのDockerfileを使って、サイドカー用のイメージを作成し、ECRにPushします。

{
  "containerDefinitions": [
    {
      ### 本体のログ設定をawsfirelensにする
      "logConfiguration": {
        "logDriver": "awsfirelens",
        "secretOptions": null,
        "options": {
          "Name": "stdout"
        }
      },
      "volumesFrom": []
    },
    ###  サイドカー用の設定 追加 ###
    {
     ### 上でPushしたイメージを指定
      "image": "*********.dkr.ecr.ap-northeast-1.amazonaws.com/fluent-bit-test:latest",
      "firelensConfiguration": {
        "type": "fluentbit",
        "options": {
          "config-file-type": "file",
          "config-file-value": "/fluent-bit/etc/*********.conf"
        }
      },
      "name": "log_router"
    }
    ###  サイドカー用の設定 追加 ###
  ]
}

ECS側の設定はこんな感じです。簡単ですね。

4. メトリクス監視

ECSやRedisなどのメトリクスの監視を行います。 メモリ、CPUの使用率が閾値超えたら、オートスケールを行いつつ、Slackにも通知がくるようにしました。

ログと同じく、メトリクスフィルターでコンテナやRedisの使用率を測って、閾値超えたらアラートが発砲するようになっています。

通知の流れは、メトリクスフィルターで検知した閾値越えを、エラーログと同じフローでSlackまで流しています。

5. 外形監視

Synthetics Canariesを使用して、コンテナの外形監視を行います。 ヘルスチェック用のエンドポイントに向けて、リクエストを投げて、監視しています。 具体的に設定しているの以下の通りです。

const { URL } = require('url');
const synthetics = require('Synthetics');
const log = require('SyntheticsLogger');
const syntheticsConfiguration = synthetics.getConfiguration();
const syntheticsLogHelper = require('SyntheticsLogHelper');
 
const loadBlueprint = async function () {
     // ヘルスチェック用のエンドポイントを指定
    const urls = ['https://xxxxx/check'];
    ~~~ 中略 ~~~
}
 
const loadUrl = async function (page, url, takeScreenshot) {
    ~~~ 中略 ~~~
    }
    
    await synthetics.executeStep(stepName, async function () {
        const sanitizedUrl = syntheticsLogHelper.getSanitizedUrl(url);
        domcontentloaded = true;
       // 戻り値を検証する
        page.goto(url, { waitUntil: ['domcontentloaded'], timeout: 30000})
        .then(response => {
            const status = response.status();
            const statusText = response.statusText();
            logResponseString = `Response from url: ${sanitizedUrl}  Status: ${status}  Status Text: ${statusText}`;
        
            //If the response status code is not a 2xx success code
            if (response.status() < 200 || response.status() > 299) {
                throw `Failed to load url: ${sanitizedUrl} ${response.status()} ${response.statusText()}`;
            }
            return response.json()
        })
        .then(data => {
            if (data.Status  != "SERVING") {
                throw `Bad Status :${response.json()}`
            }
        })
    });
    
    ~~~以下略~~~

通知のフローはこちらもメトリクスフィルターで異常を拾ってSlackまで流し込んでいます。

5. まとめ

この仕組みを作って運用を開始し、お客様からお問い合わせいただく前にエンジニアがエラーについて検知し、 予め確認することができたり、大分運用がしやすくなったと思います。 ただ、まだ改善すべき点がある仕組みだと思いますので、これからも改善していきたいです。 導入してみて、成果があった監視や記事に関して、ご意見・ご感想を頂けますと幸いでございます。

参考

mazyu36.hatenablog.com