エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

Auto Scaling に対応した EC2 監視アラーム設定ツール

こんにちは、エムスリー 兼 QLife エンジニアの園田です。

昨日、小ネタで投稿した Auto-Scaling 配下の EC2 の CloudWatch Alarm 設定ツールを SAM の OSS として公開したので、その説明のポストとなります。

github.com

昨日の小ネタ記事はこちらです。
(小ネタ) AutoScaling で増減した EC2 インスタンスに動的に CloudWatch Alarm を設定 - エムスリーテックブログ

なぜ実装したか

エムスリーグループの QLife では、オンプレのインフラ環境を2018年の9月に AWS へ完全移行しました。

移行後の環境は ZABBIX 3 や Mackerel の監視が導入されていますが、新規サービスなどで AWS アカウントや VPC を追加した場合に、監視設定業務が ZABBIX のノウハウに依存し、属人化してしまいます。Mackerel などの外部サービスの場合、ホスト課金も増えてしまいます。

また、ZABBIX は Auto-Scaling に弱く、ホスト自動登録を行うためには(ノウハウ自体はありふれているが)標準機能にないハックが必要となります。

他の監視の仕組み( Prometheus など)も検討しましたが、どうせ監視システムの運用コストがかかるなら内製してしまえ!とカッとなって実装した次第であります。

また、運用監視の要件として、ホストごとの監視を必須とする場合(官公庁とかだとよくある)にもマッチするかと思われます。

ツール概要

S3 に監視定義用の JSON または YAML ファイルを置いて、Launch Template や Launch Configration のタグにそのファイルへの参照を設定します。 Auto-Scaling のインスタンス起動イベントを CloudWatch Event Rule で検知して Lambda を実行し、タグの値に紐付いた監視定義をインスタンスに対して適用(CloudWatch Alarm を作成)します。 インスタンス削除時も同様に、タグから定義を参照して当該アラームを削除します。

f:id:ryoheisonoda:20181226124017p:plain

説明だけだと意味不明だと思いますので、実際の監視設定手順を書きます。

監視定義 適用手順

1. リポジトリを clone

git clone https://github.com/QLife-Inc/ec2-auto-metric-alarms.git
cd ec2-auto-metric-alarms/

2. 監視定義ファイルを格納するバケットを作成

こちらは既存のバケットでも構いません。

aws s3api create-bucket \
  --bucket "your_bucket_name" \
  --acl private

3. 監視定義ファイルをアップロード

リポジトリの sample-definitions にサンプルがあるので、それを利用します。YAML を利用する場合は --content-type text/yaml を指定します。

aws s3api put-object \
  --bucket "your_bucket_name" \
  --key "your_prefix/cpu-util" \
  --body "sample-definitions/cpu-util.yml" \
  --content-type "text/yaml"

4. 監視を設定するインスタンスにタグを付与

AutoScalingGroup で利用する Launch Template や Launch Configuration の設定で EC2 インスタンスに付与するタグを追加します。 また、すでに起動済みのインスタンスに対して監視を適用する場合はインスタンスにも直接タグを付与する必要があります。 Auto-Scaling を介さずに動作確認を行う場合もインスタンスにタグを直接付与してください。

デフォルトのキーは AlarmDefinitionName です。この値に、先ほど S3 にアップロードしたオブジェクトのキー(ファイル名)を設定することで、インスタンスと監視定義を紐づけます。

f:id:ryoheisonoda:20181226101230p:plain

5. 通知先の SNS トピックを作成

緊急通知用と通常通知用の2つのトピックを作成します。トピック名はなんでも構いません。

aws sns create-topic --name "system-emergency-notification"
aws sns create-topic --name "system-notification"

トピックが作成できたら、適当なメールアドレスをサブスクリプションとして登録しておきます。

6. ツール(SAM)をデプロイ

sam コマンドがインストールされていない場合はインストールしてください。

pip install --user --upgrade aws-sam-cli # brew install aws-sam-cli

ソースをパッケージングします。sam package コマンドを実行すると、指定した S3 バケットに SAM のソースアーカイブがアップロードされます。このアップロードされたアーカイブから CloudFormation により Lambda 関数が作成されます。また、 template.yaml が CloudFormation テンプレートとしてコンパイルされます。

sam package --template-file "template.yaml" \
  --output-template-file "cloudformation.yml" \
  --s3-bucket "your_source_bucket" --s3-prefix "sam-sources"

sam deploy コマンドを実行します。このコマンドで、 package コマンドによって生成された cloudformation.yml から CloudFormation の Stack が作成されます。 コマンド実行時に CloudFormation のパラメータを渡す必要があります。 template.yamlParameters ディレクティブで Default が設定されているパラメータ以外は必須になります。

sam deploy --capabilities CAPABILITY_NAMED_IAM \
  --stack-name "your_stack_name" \
  --template-file "cloudformation.yml" \
  --parameter-overrides \
    DefinitionBucketName="your_bucket_name" \
    DefinitionObjectPrefix="your_prefix/" \
    FunctionName="ec2-auto-metric-alarms" \
    EmergencyTopicArn=${5.で作成した緊急通知先トピックのARN} \
    OrdinaryTopicArn=${5.で作成した通常通知先トピックのARN}

7. ツール実行

SAM のデプロイが成功すると Lambda 関数が作成されているはずですので、Lambda のコンソールから直接実行してみます。 このツールの挙動として、Auto-Scaling 以外のイベントデータを受け取った場合、現在起動中の監視定義タグが設定されているすべてのインスタンスに対して監視定義を更新するため、テストイベントデータは空の JSON で問題ありません。 作成された Lambda 関数の画面から、「テストイベントの設定」を開いて、空のイベントデータを設定します。

f:id:ryoheisonoda:20181226105019p:plain

作成したテストイベントを選択して「テスト」ボタンを押して正常終了すれば、4. でタグを設定したインスタンスに対する CloudWatch Alarm が生成されていることが確認できます。

f:id:ryoheisonoda:20181226110426p:plain

以上が簡単な監視定義の適用手順となります。

監視定義ファイルの解説

README にもモデルのスキーマについて説明がありますが、より詳しい説明を書きます。

このツールで利用する監視定義ファイルに記載する監視定義は CloudWatch の PutMetricAlarm API のリクエストモデルを拡張したモデルで、 JSON または YAML で定義します。

先ほどの手順でサンプルとして利用した cpu-util の監視定義ファイルは以下のようになっています。

---
_cpu_util: &cpu_util
  namespace: AWS/EC2
  dimensions:
  - name: InstanceId
    value: <%= instance.id %>
  metric_name: CPUUtilization
  statistic: Average
  threshold: 90
  unit: Percent
  comparison_operator: GreaterThanThreshold
  period: 300
  treat_missing_data: ignore

name: cpu-util
description: EC2 の CPU 監視テンプレート
alarm_definitions:
- <<: *cpu_util
  alarm_id: notice-cpu-util
  notification_type: ordinary
  alarm_name: ec2-<%= instance.id %>-notice-cpu-util
  alarm_description: <%= instance.name %> CPU使用率が 10 分間 90 % を超えていたら通知
  evaluation_periods: 2
  datapoints_to_alarm: 2
- <<: *cpu_util
  alarm_id: emergency-cpu-util
  notification_type: emergency
  alarm_name: ec2-<%= instance.id %>-emergency-cpu-util
  alarm_description: <%= instance.name %> CPU使用率が 30 分間 90 % を超えていたら通知
  evaluation_periods: 6
  datapoints_to_alarm: 6

よく見ると、 <%= instance.id %> などが登場して、 ERB のテンプレートになっていることがわかります。 この instanceAws::EC2::Instance のラッパーインスタンスで、 Lambda 実行時に埋め込まれるため、同じ監視定義でも AlarmName が重複することなく、複数の EC2 インスタンスの監視定義を作成できるようにしています(CloudWatch Alarm は Region と AlarmName で一意になる)。

監視定義の再利用

EC2 の監視といっても、そのインスタンスの用途によって監視内容は様々です。 例えば全ての EC2 で監視するメトリクス、ElasticBeanstalk のインスタンスで監視するメトリクス、ECS Cluster のインスタンスで監視するメトリクスなど、同じ EC2 というサービスでも異なるメトリクスに対する複数の監視定義が必要になることが多々あります。 それらについて、共通の監視定義を毎回実装するのは運用上避けたいので、他の監視定義をincludeすることで定義の継承を可能としています。

例えば、前述の CPU 監視とは別に、EC2 ステータスチェックの監視テンプレートがあるとします。

---
name: ec2-status
description: EC2 の ステータス 監視テンプレート
alarm_definitions:
- alarm_id: notice-status-check
  notification_type: ordinary
  alarm_name: ec2-<%= instance.id %>-notice-status-check
  alarm_description: <%= instance.name %> ステータスチェックが失敗したら通知
  namespace: AWS/EC2
  dimensions:
  - name: InstanceId
    value: <%= instance.id %>
  metric_name: StatusCheckFailed
  statistic: Maximum
  threshold: 1
  comparison_operator: GreaterThanThreshold
  period: 300
  evaluation_periods: 1
  datapoints_to_alarm: 1
  treat_missing_data: ignore

CPU 監視とステータス監視はすべての EC2 インスタンスで利用したい、というユースケースの場合、以下のようにアップロード済みの監視定義を継承することができます。

---
name: ec2-default
description: EC2 の共通監視テンプレート
include_definitions: [ cpu-util, ec2-status ]

ステージング環境など、緊急通知を送信したくないインスタンスに対しては以下のように緊急通知だけを除外した定義を利用できます。

---
name: ec2-default-without-emergency
description: EC2 の共通監視テンプレート(緊急通知除外)
include_definitions: [ ec2-default ]
ignore_alarm_ids: [ emergency-cpu-util ]

このように監視要件に合わせた監視定義の ERB テンプレートを監視対象となるサーバロールごとに実装して、 S3 にアップロードしてタグで参照してあげれば OK です。

エンジニア募集!!

QLife では共にチーム一丸となってより良いものづくりにこだわれる仲間を募集中です! 小さいサービスが多いので新しい AWS のサービスの利用にも非常に積極的に取り組んでいます! カジュアル面談も行っていますので、興味がある方は entry@qlife.co.jp に「カジュアル面談希望」とメールをください!

www.qlife.co.jp

エムスリーでもエンジニアを随時募集しています!共に医療 × テクノロジーの未来を切り拓いてくれる仲間を募集中です! AWS 以外にも GCP や Firebase などのクラウドも活用しています!興味がある方はカジュアル面談やTechtalkにおこしください!!

jobs.m3.com

(小ネタ) AutoScaling で増減した EC2 インスタンスに動的に CloudWatch Alarm を設定

こんにちは、エムスリーエンジニアの園田です。

AWS の AutoScaling で増減する EC2 インスタンスに対して CloudWatch Alarm を動的に設定したくなることありますよね?

AutoScalingGroup のメトリクスで AutoScalingGroup 内の平均 CPU 利用率などを監視することはできますが、 個々のインスタンスそれぞれに対してアラームを設定することは(今のところ)できません。
正確には、設定したとしても AutoScaling で新しく起動したインスタンスには設定されませんし、 スケールインで削除されたからといって自動でアラームの設定が削除されることもありません。

そこで、 CloudWatch Event Rule と Lambda を使って増減するインスタンスに対して動的にアラームを設定するのを試してみました。(何番煎じかわかりませんが・・・)

なお、この記事は エムスリーアドベントカレンダー ではないです。

監視シナリオ

今回は、以下のシナリオを例として実装してみます。

EC2 インスタンスのCPU使用率が 10 分間 80 % を超えていたら、SNSトピックに通知する。

イベントデータの JSON 確認

Lambda を実装する上で、CloudWatch Event からどういったイベントデータを受け取れるのかを確認しなければいけません。

イベントデータの JSON を確認するために、マネジメントコンソールで CloudWatch > イベント > ルール の ルールの作成 ボタンをポチります。ルールの作成画面で以下のように設定すると、サンプルイベントデータが表示されるので、コピっておきます。

f:id:ryoheisonoda:20181207165540p:plain

表示されたサンプルイベントデータは以下の通りです。

{
  "version": "0",
  "id": "3e3c153a-8339-4e30-8c35-687ebef853fe",
  "detail-type": "EC2 Instance Launch Successful",
  "source": "aws.autoscaling",
  "account": "123456789012",
  "time": "2015-11-11T21:31:47Z",
  "region": "us-east-1",
  "resources": [
    "arn:aws:autoscaling:us-east-1:123456789012:autoScalingGroup:eb56d16b-bbf0-401d-b893-d5978ed4a025:autoScalingGroupName/sampleLuanchSucASG",
    "arn:aws:ec2:us-east-1:123456789012:instance/i-b188560f"
  ],
  "detail": {
    "StatusCode": "InProgress",
    "AutoScalingGroupName": "sampleLuanchSucASG",
    "ActivityId": "9cabb81f-42de-417d-8aa7-ce16bf026590",
    "Details": {
      "Availability Zone": "us-east-1b",
      "Subnet ID": "subnet-95bfcebe"
    },
    "RequestId": "9cabb81f-42de-417d-8aa7-ce16bf026590",
    "EndTime": "2015-11-11T21:31:47.208Z",
    "EC2InstanceId": "i-b188560f",
    "StartTime": "2015-11-11T21:31:13.671Z",
    "Cause": "At 2015-11-11T21:31:10Z a user request created an AutoScalingGroup changing the desired capacity from 0 to 1.  At 2015-11-11T21:31:11Z an instance was started in response to a difference between desired and actual capacity, increasing the capacity from 0 to 1."
  }
}

今回利用するのは、起動したのか削除されたのかを判別するための $.detail-type と、インスタンスを特定するための $.detail.EC2InstanceId だけです。

Lambda 関数の実装

これを参考に、Lambda を実装してみます。せっかくなので、最近使えるようになった Ruby で実装しました。
追加ライブラリを利用していないため、パッケージのアップロードは不要です。

require 'json'
require 'aws-sdk'

# 通知先の SNS トピックは Lambda の環境変数に設定しておく
TOPIC_ARN = ENV['TOPIC_ARN']

# 実際にプロダクション利用する場合はイベントデータから受け取ったリージョンを指定してください
EC2 = Aws::EC2::Resource.new
CW = Aws::CloudWatch::Resource.new

# サンプルイベントデータを参考に、eventから必要な値を取得して処理する
def lambda_handler(event:, context:)
    instance_id = event['detail']['EC2InstanceId']

    case event['detail-type']
        # スケールアウトして新しくインスタンスが起動した場合
        when 'EC2 Instance Launch Successful'
            on_launched(instance_id)
        # スケールインしてインスタンスが削除された場合
        when 'EC2 Instance Terminate Successful'
            on_terminated(instance_id)
    end
end

def on_launched(instance_id)
    instance = EC2.instance(instance_id)

    return unless instance.exists?
    return if CW.alarm("ec2-#{instance.id}-cpu-util-notification").exists?

    # CloudWatch Alarm を作成
    CW.client.put_metric_alarm({
        alarm_name: "ec2-#{instance.id}-cpu-util-notification",
        alarm_description: "CPU使用率が 10 分間 80 % を超えていたら通知",
        # EC2
        namespace: 'AWS/EC2',
        # インスタンスを指定
        dimensions: [
            { name: "InstanceId", value: instance.id }
        ],
        # CPU 使用率の平均
        metric_name: 'CPUUtilization',
        statistic: "Average",
        # > 80 %
        threshold: 80,
        unit: 'Percent',
        comparison_operator: "GreaterThanThreshold",
        # 10 分間 (5 分 * 2 回) のうち 2 回
        period: 300,
        evaluation_periods: 2,
        datapoints_to_alarm: 2,
        # データなしは無視
        treat_missing_data: "ignore",
        # 閾値を超えたら警告通知
        alarm_actions: [ TOPIC_ARN ],
        # 元に戻ったことも通知
        ok_actions: [ TOPIC_ARN ],
    })
end

# インスタンスが削除されたらアラームも削除
def on_terminated(instance_id)
    alarm = CW.alarm("ec2-#{instance_id}-cpu-util-notification")
    alarm.delete if alarm.exists?
end

Lambda の実行ロールには以下の権限をつけておきます。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:*:*:*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:DescribeInstances",
                "cloudwatch:PutMetricAlarm",
                "cloudwatch:DeleteAlarms",
                "cloudwatch:DescribeAlarms"
            ],
            "Resource": "*"
        }
    ]
}

サンプルイベントデータをテストイベントに設定して、インスタンスIDだけ実在するIDに変更して Lambda をテスト実行してみると、CloudWatch Alarm が作成されます。

aws cloudwatch describe-alarms --alarm-name-prefix ec2-
{
    "MetricAlarms": [
        {
            "AlarmName": "ec2-i-xxxxxxxxxxxxxxxx-cpu-util-notification",
            "AlarmArn": "arn:aws:cloudwatch:ap-northeast-1:999999999999:alarm:ec2-i-xxxxxxxxxxxxxxxx-cpu-util-notification",
            "AlarmDescription": "CPU使用率が 10 分間 80 % を超えていたら通知",
            "AlarmConfigurationUpdatedTimestamp": "2018-12-07T08:19:33.703Z",
            "ActionsEnabled": true,
            "OKActions": [
                "arn:aws:sns:ap-northeast-1:999999999999:system-notification"
            ],
            "AlarmActions": [
                "arn:aws:sns:ap-northeast-1:999999999999:system-notification"
            ],
            "InsufficientDataActions": [],
            "StateValue": "OK",
            "StateReason": "Threshold Crossed: 2 out of the last 2 datapoints [0.0 (07/12/18 08:14:00), 0.0 (07/12/18 08:09:00)] were not greater than the threshold (80.0) (minimum 2 datapoints for ALARM -> OK transition).",
            "StateReasonData": "{\"version\":\"1.0\",\"queryDate\":\"2018-12-07T08:19:34.301+0000\",\"startDate\":\"2018-12-07T08:09:00.000+0000\",\"unit\":\"Percent\",\"statistic\":\"Average\",\"period\":300,\"recentDatapoints\":[0.0,0.0],\"threshold\":80.0}",
            "StateUpdatedTimestamp": "2018-12-07T08:19:34.311Z",
            "MetricName": "CPUUtilization",
            "Namespace": "AWS/EC2",
            "Statistic": "Average",
            "Dimensions": [
                {
                    "Name": "InstanceId",
                    "Value": "i-xxxxxxxxxxxxxxxx"
                }
            ],
            "Period": 300,
            "Unit": "Percent",
            "EvaluationPeriods": 2,
            "DatapointsToAlarm": 2,
            "Threshold": 80.0,
            "ComparisonOperator": "GreaterThanThreshold",
            "TreatMissingData": "ignore"
        }
    ]
}

テストデータの detail-typeEC2 Instance Terminate Successful に変更して実行すれば、アラームが削除されることも確認できます。

CloudWatch Event Rule の作成

Lambda 関数ができたら、先ほど作りかけた CloudWatch Event Rule を作成します。

イベントタイプに EC2 Instance Launch SuccessfulEC2 Instance Terminate Successful を指定しています。

f:id:ryoheisonoda:20181207172807p:plain

まとめ

CloudWatch Event Rule と Lambda の組み合わせで AutoScalingGroup で増減するインスタンスに動的にアラームを設定できました。今回は CPU使用率でしたが、 CloudWatch Agent と組み合わせればメモリやディスクの監視通知も可能ですし、ECS タスクでも同様のことができると思います。

2018/12/27 追記

この仕組みを利用した SAM (Serverless Application Model) を OSS として公開しました。 公開した SAM については後日またこのテックブログにポストしたいと思います。

エンジニア募集!!

エムスリーでは、共に医療 × テクノロジーの未来を切り拓いてくれる仲間を募集中です! AWS 以外にも GCP や Firebase などのクラウドも活用しています!興味がある方はカジュアル面談やTechtalkにおこしください!!

jobs.m3.com

医療用語に注目した文書の類似度計算(SCDV+XGBoost)

はじめに

エンジニアリングGの西場(@m_nishiba)です。 エムスリー Advent Calendar 2018に参加しています。

文書間の類似度計算をしたい。

エムスリーでは20以上のサービスを展開しています。各サービスごとにコンテンツのフォーマットが大きく異なるのですが、横断して類似するコンテンツを表示したいというニーズがあります。

続きを読む

エムスリーエンジニアリングGの組織構成について

エムスリーエンジニアリンググループ(以下エンジニアリングG)の山崎です。この記事は エムスリー Advent Calendar 2018 の24日目の記事です。

私自身はエンジニアバックグラウンドのプロダクトマネージャですが、昨年12/1よりVPoEとして、今年の4/1より業務執行役員としてエムスリー及びグループ会社の経営に関わっています。

本日はカジュアル面談や採用面接などでもよく聞かれるエンジニアリングGの組織構成について、掘り下げてご紹介できればと思います。

続きを読む

React.js, Vue.jsが使えない状況でメンテナンス性の高いJavaScriptを書く3つのポイント

エムスリー エンジニアの岩本です。 この記事は エムスリー Advent Calendar 2018 の23日目の記事です。

React.jsやVue.jsを使えれば、開発のベストプラクティスなどがあるので、メンテナンス性の高いプログラムはずいぶんと書きやすくなったと思います。本当に仮想DOMの功績は大きいですね。

しかし、世の中にはそういったライブラリを使うことができないプロジェクトもあるわけです。古すぎて、一部分だけ最新のソースコードにすることが憚られたり、サイズの問題でライブラリを入れることができなかったり。。。

その場合どのように書けばメンテナンス性の高いプログラムを書くことができるのでしょうか。そこでIE6時代からJavaScriptをもりもりと書いている私なりのベストプラクティスを紹介します。

続きを読む

JUnit5を使おう

f:id:taknakamura:20181218221804p:plain

エムスリー エンジニアの中村です。 この記事は エムスリー Advent Calendar 2018 の22日目の記事です。

JavaでのテストフレームワークといえばJUnitですが、最新版のJUnit5がかなり使いやすくなっています。 既に多くのJUnit5紹介記事がありますので、改めて私が細かく説明することもないかと思いますが、個人的に嬉しいと思った機能を中心に紹介したいと思います。

JUnit5

junit.org

JUnit5 は初版がリリースされてから1年がすぎて、普通に使うには十分な環境になっているのではないかと思います。 Intellij IDEAでもサポートされ、メソッド単位のテスト実行や実行したテストケースの一覧表示など、これまでのJUnit4と変わらずに使うことができます。 Springのテストモージュルでも対応され、JUnit5向けのテスト拡張クラスが提供されています。私が簡易に試した限りではMockMVCなども問題なく使えました。

経緯

  • 2015年 開発開始
  • 2017年9月 v5.0.0 リリース
  • 2018年12月現在 v5.3.2リリース中

状況

  • Java8以降をサポート
  • Intellij IDEAでもサポート
  • Springのテストモジュールは標準はJUnit4だが、JUnit5対応もあり
    • MockMVCなども利用可能
続きを読む

JJUG CCC 2018 Fallで登壇しました

この記事はエムスリー Advent Calendar 2018 の21日目の記事です。

こんにちは。エンジニアリンググループの滝安(@juntaki)です。
先日のJJUG CCC 2018 Fallで「エムスリーでのKotlinへの取り組み」と題して、スポンサーセッションで星川(@oboenikui)と2名で登壇しました。

続きを読む