ServiceNowとElasticsearchを使ってインシデントマネジメントを実施する

ブログシリーズのパート2へようこそ。パート1の記事では、ServiceNowとElasticsearchに双方向通信をセットアップしました。ServiceNowの設定がメインだった前回と対照的に、今回はElasticsearchとKibanaに関する作業が中心となります。この記事を最後までお読みいただくと、この2つのパワフルなアプリケーションの組み合わせで、インシデントマネジメントが非常に楽になります。つまり、少なくとも従来より大幅に簡単になります。

他のあらゆるElasticsearchプロジェクトと同様、まずニーズに合致したマッピングのインデックスを作成します。このプロジェクトには、以下のデータを保持するインデックスが必要となります。

  • ServiceNowのインシデントアップデート(servicenow-incident-updates):ServiceNowからElasticsearchに送られる情報をすべて格納します。このインデックスは、ServiceNowがプッシュすることによりアップデートされます。
  • 利便性を目的としたアプリケーションのアップタイムサマリー(app_uptime_summary):各アプリケーションがオンラインとなっていた合計時間を格納します。利便性のため、中間データの状態にしていると考えてください。
  • アプリケーションインシデントのサマリー(app_incident_summary):各アプリケーションに生じたインシデント数と、各アプリケーションのアップタイム、および、各アプリケーションの最新のMTBF(平均故障間隔)を格納します。

最後の2つのインデックスはパート3で作成するCanvas workpadの毎回の更新で複雑なロジックを大量に実行する状況を回避するためのヘルパーインデックスです。2つのインデックスは、変換経由で継続的にアップデートされます。

3つのインデックスを作成する

インデックスの作成にあたって、以下のガイダンスを使用します。以下の手順で使用するインデックス名と異なるインデックス名を使用される場合、ServiceNowのセットアップも調整する必要がある場合があることに留意してください。

servicenow-incident-updates

ベストプラクティスに従い、まずインデックスエイリアスを、次にインデックスライフサイクル管理(ILM)ポリシーをセットアップします。またこのILMポリシーによって今後インデックスが作成される場合に同一のマッピングを適用するため、インデックステンプレートも作成します。今回セットアップするILMポリシーは、「インデックス内に50GBのデータが格納されると新規のインデックスを作成し、元のインデックスは1年後に削除する」というものです。インデックスエイリアスを使うと、新しいインデックスが作成されたときに、ServiceNowのbusiness ruleをアップデートしなくても簡単にデータを新しいインデックスに送ることができます。 

# ILMポリシーを作成します 
PUT _ilm/policy/servicenow-incident-updates-policy 
{ 
  "policy": { 
    "phases": { 
      "hot": {                       
        "actions": { 
          "rollover": { 
            "max_size":"50GB" 
          } 
        } 
      },
      "delete": { 
        "min_age":"360d",           
        "actions": { 
          "delete": {}              
        } 
      } 
    } 
  } 
}
# インデックステンプレートを作成します 
PUT _template/servicenow-incident-updates-template 
{ 
  "index_patterns": [ 
    "servicenow-incident-updates*" 
  ],
  "settings": { 
    "number_of_shards":1,
    "index.lifecycle.name": "servicenow-incident-updates-policy",      
    "index.lifecycle.rollover_alias": "servicenow-incident-updates"     
  },
  "mappings": { 
    "properties": { 
      "@timestamp": { 
        "type": "date",
        "format": "yyyy-MM-dd HH:mm:ss" 
      },
      "assignedTo": { 
        "type": "keyword" 
      },
      "description": { 
        "type": "text",
        "fields": { 
          "keyword": { 
            "type": "keyword",
            "ignore_above":256 
          } 
        } 
      },
      "incidentID": { 
        "type": "keyword" 
      },
      "state": { 
        "type": "keyword" 
      },
      "app_name": { 
        "type": "keyword" 
      },
      "updatedDate": { 
        "type": "date",
        "format": "yyyy-MM-dd HH:mm:ss" 
      },
      "workNotes": { 
        "type": "text" 
      } 
    } 
  } 
}
# 初期インデックスをブートストラップし、エイリアスを作成します 
PUT servicenow-incident-updates-000001 
{ 
  "aliases": { 
    "servicenow-incident-updates": { 
      "is_write_index": true 
    } 
  } 
}

app_uptime_summary & app_incident_summary

この2つのインデックスはどちらもentity-centric(エンティティ中心的)なので、関連のILMポリシーを設定する必要はありません。これは、監視するアプリケーション1つにつき1ドキュメントしかないためです。これらのインデックスを作成するには、以下のコマンドを発行します。

PUT app_uptime_summary 
{ 
  "mappings": { 
    "properties": { 
      "hours_online": { 
        "type": "float" 
      },
      "app_name": { 
        "type": "keyword" 
      },
      "up_count": { 
        "type": "long" 
      },
      "last_updated": { 
        "type": "date" 
      } 
    } 
  } 
}
PUT app_incident_summary 
{ 
  "mappings": { 
    "properties" : { 
        "hours_online" : { 
          "type" : "float" 
        },
        "incident_count" : { 
          "type" : "integer" 
        },
        "app_name" : { 
           "type" : "keyword" 
        },
        "mtbf" : { 
          "type" : "float" 
        } 
      } 
  } 
}

2つの変換をセットアップする

変換は驚くほど便利な機能であり、Elastic Stackに最近追加されました。変換を使うと、既存のインデックスをentity-centricなサマリーに転換でき、分析新たなインサイトを抽出する場面ですぐれた能力を発揮します。さらに、見落とされやすい側面としてパフォーマンスメリットがあります。たとえば各アプリケーションをクエリしてからアグリゲーションしてMTBFを算出する(つまり、非常に複雑なアプローチを実行する)のではなく、指定した時間間隔でこのメトリックを継続的に算出することができます。時間間隔を1分ごとに設定することも可能です。変換を使用しない場合、メトリックは各ユーザーのCanvas workpadが更新されるたびに算出されることになります。たとえば50人の社員全員が30秒間隔で更新されるworkpadを使用すると、費用負荷を生じるクエリを1分に100回実行することになり、過剰感があります。これは、現実にElasticsearchを使うほとんどのケースで問題とならない程度の使用量ですが、処理を楽にするすぐれた新機能が登場したのですから、活用しない手はありません。

以下の手順で、2つの変換を作成します。 

  • calculate_uptime_hours_online_transform: 各アプリケーションがオンラインであり、かつ応答していた合計時間を算出します。計算には、Heartbeatからくるアップタイムデータを使用します。算出した値はapp_uptime_summaryインデックスに格納されます。 
  • app_incident_summary_transform:ServiceNowデータと上記の変換からくるアップタイムデータを組み合わせます(join field typeのようなイメージです)。この変換は、アップタイムデータを収集して各アプリケーションに生じたインシデント数を分析し、オフラインの合計時間を計算するほか、最終的にこの2つのメトリックに基づくMTBFを算出します。この結果は、app_incident_summaryという名前のインデックスに格納されます。

calculate_uptime_hours_online_transform

PUT _transform/calculate_uptime_hours_online_transform 
{ 
  "source": { 
    "index": [ 
      "heartbeat*" 
    ],
    "query": { 
      "bool": { 
        "must": [ 
          { 
            "match_phrase": { 
              "monitor.status": "up" 
            } 
          } 
        ] 
      } 
    } 
  },
  "dest": { 
    "index": "app_uptime_summary" 
  },
  "sync": { 
    "time": { 
      "field": "@timestamp",
      "delay":"60s" 
    } 
  },
  "pivot": { 
    "group_by": { 
      "app_name": { 
        "terms": { 
          "field": "monitor.name" 
        } 
      } 
    },
    "aggregations": { 
      "@timestamp": { 
        "max": { 
          "field": "@timestamp" 
        } 
      },
      "up_count": { 
        "value_count": { 
          "field": "monitor.status" 
        } 
      },
      "hours_online": { 
        "bucket_script": { 
          "buckets_path": { 
            "up_count": "up_count" 
          },
          "script": "(params.up_count * 60.0) / 3600.0" 
        } 
      } 
    } 
  },
  "description":"Calculate the hours online for each thing monitored by uptime" 
}

app_incident_summary_transform 

PUT _transform/app_incident_summary_transform 
{ 
  "source": { 
    "index": [ 
      "app_uptime_summary",
      "servicenow*" 
    ] 
  },
  "pivot": { 
    "group_by": { 
      "app_name": { 
        "terms": { 
          "field": "app_name" 
        } 
      } 
    },
    "aggregations": { 
      "incident_count": { 
        "cardinality": { 
          "field": "incidentID" 
        } 
      },
      "hours_online": { 
        "max": { 
          "field": "hours_online",
          "missing":0 
        } 
      },
      "mtbf": { 
        "bucket_script": { 
          "buckets_path": { 
            "hours_online": "hours_online",
            "incident_count": "incident_count" 
          },
          "script": "(float)params.hours_online / (float)params.incident_count" 
        } 
      } 
    } 
  },
  "description":"Calculates the MTBF for apps by using the output from the calculate_uptime_hours_online transform",
  "dest": { 
    "index": "app_incident_summary" 
  },
  "sync": { 
    "time": { 
      "field": "@timestamp",
      "delay":"1m" 
    } 
  } 
}

2つの変換が正常に実行されていることを確認しましょう。

POST _transform/calculate_uptime_hours_online_transform/_start 
POST _transform/app_incident_summary_transform/_start

アップタイムアラートでServiceNowにチケットを作成する

Canvas workpadに関する工程は別として、このループの最終段階は、障害の既存のチケットがない場合にServiceNowで実際のチケットを作成するステップです。そのために、ここではWatcherを使用してアラートを作成します。アラートは、以下のような複数のステップで構成されます。前提として、Watcherは1分ごとに実行されます。Heartbeatのドキュメントで、すべてのアップタイムフィールドについて確認することができます。

1. 直近5分間にダウンしていたアプリケーションを確認する

このステップはシンプルです。直近の5分間(範囲フィルターを適用)にダウン(用語フィルターを適用)していたHeartbeatイベントをmonitor.name別に分けて(用語アグリゲーションを適用)すべて収集します。このmonitor.nameフィールドはElastic Common Schema(ECS)に対応しており、application nameフィールドの値と同義になります。ここまでのすべてのプロセスはWatcherのdown_checkインプット(下記参照)経由で実行されます。

2. 各アプリケーションの上位20チケットを入手し、各チケットの最新アップデートを把握する

このステップは少々複雑です。ServiceNowのbusiness ruleによって自動でインジェストされたServiceNowデータを検索します。ここで用いるexisting_ticket_checkインプットは、複数のアグリゲーションを使用します。はじめに“apps”という用語でアグリゲーションし、すべてのアプリケーションをまとめてグループ化します。さらに各アプリをServiceNowチケットのインシデントIDでグループ化します。これは、incidentsと呼ばれる用語アグリゲーションを使って実行します。最後に、各アプリケーションで見つかった個々のインシデントについてtop_hitsアグリゲーションを使い、@timestampフィールドで並べることにより、最新の状態を把握します。 

3. 2つのフィードを結合し、チケット作成の必要性を判断する

この目的には、script payload変換を使用します。簡単に説明すると、down_checkアウトプットを反復してダウンしているアプリケーションを確認し、次に特定のアプリケーションに未解決のチケットがあるかを確認するという作業です。この変換は現在“未解決”、“新規”、または“一時停止中”のチケットがないアプリケーションをリストに追加します。変換が返したリストは、アクションフェーズに渡されます。 

このプロセスにおいて、payload変換は以下のようなエッジケースを捕捉するために、インシデント履歴が1つもないアプリであるかなどの多数のチェックを実行します。この変換のアプトプットは、アプリケーション名の配列になります。

4. 新規のアラートの場合、ServiceNowにチケットを作成する

ServiceNowが提供するREST APIを使い、WebフックアクションでServiceNowにチケットを作成します。このAPIは前のステップで生成した配列のアプリケーション名に対してforeachパラメーターを反復適用し、その後各々に対してWebフックアクションを実行します。このステップは、チケットを必要とするアプリケーションが1つ以上存在する場合に実行されます。ServiceNowで正しい認証情報とエンドポイントが設定されていることを確認してください。

PUT _watcher/watch/e146d580-3de7-4a4c-a519-9598e47cbad1 
{ 
  "trigger": { 
    "schedule": { 
      "interval":"1m" 
    } 
  },
  "input": { 
    "chain": { 
      "inputs": [ 
        { 
          "down_check": { 
            "search": { 
              "request": { 
                "body": { 
                  "query": { 
                    "bool": { 
                      "filter": [ 
                        { 
                          "range": { 
                            "@timestamp": { 
                              "gte": "now-5m/m" 
                            } 
                          } 
                        },
                        { 
                          "term": { 
                            "monitor.status": "down" 
                          } 
                        } 
                      ] 
                    } 
                  },
                  "size":0,
                  "aggs": { 
                    "apps": { 
                      "terms": { 
                        "field": "monitor.name",
                        "size":100 
                      } 
                    } 
                  } 
                },
                "indices": [ 
                  "heartbeat-*" 
                ],
                "rest_total_hits_as_int": true,
                "search_type": "query_then_fetch" 
              } 
            } 
          } 
        },
        { 
          "existing_ticket_check": { 
            "search": { 
              "request": { 
                "body": { 
                  "aggs": { 
                    "apps": { 
                      "aggs": { 
                        "incidents": { 
                          "aggs": { 
                            "state": { 
                              "top_hits": { 
                                "_source": "state",
                                "size":1,
                                "sort": [ 
                                  { 
                                    "@timestamp": { 
                                      "order": "desc" 
                                    } 
                                  } 
                                ] 
                              } 
                            } 
                          },
                          "terms": { 
                            "field": "incidentID",
                            "order": { 
                              "_key": "desc" 
                            },
                            "size":1 
                          } 
                        } 
                      },
                      "terms": { 
                        "field": "app_name",
                        "size":100 
                      } 
                    } 
                  },
                  "size":0 
                },
                "indices": [ 
                  "servicenow*" 
                ],
                "rest_total_hits_as_int": true,
                "search_type": "query_then_fetch" 
              } 
            } 
          } 
        } 
      ] 
    } 
  },
  "transform": { 
   "script": """ 
      List appsNeedingTicket = new ArrayList();  
      for (app_heartbeat in ctx.payload.down_check.aggregations.apps.buckets) { 
        boolean appFound = false;  
        List appsWithTickets = ctx.payload.existing_ticket_check.aggregations.apps.buckets;  
        if (appsWithTickets.size() == 0) {  
          appsNeedingTicket.add(app_heartbeat.key);  
          continue;  
        }  
        for (app in appsWithTickets) {   
          boolean needsTicket = false;  
          if (app.key == app_heartbeat.key) {  
            appFound = true;  
            for (incident in app.incidents.buckets) {  
              String state = incident.state.hits.hits[0]._source.state;  
              if (state == 'Resolved' || state == 'Closed' || state == 'Canceled') {  
                appsNeedingTicket.add(app.key);  
              }  
            }  
          }  
        }  
        if (appFound == false) {  
          appsNeedingTicket.add(app_heartbeat.key);  
        }  
      }  
      return appsNeedingTicket;  
      """ 
  },
  "actions": { 
    "submit_servicenow_ticket": { 
      "condition": { 
        "script": { 
          "source": "return ctx.payload._value.size() > 0" 
        } 
      },
      "foreach": "ctx.payload._value",
      "max_iterations":500,
      "webhook": { 
        "scheme": "https",
        "host": "dev94721.service-now.com",
        "port":443,
        "method": "post",
        "path": "/api/now/table/incident",
        "params": {},
        "headers": { 
          "Accept": "application/json",
          "Content-Type": "application/json" 
        },
        "auth": { 
          "basic": { 
            "username": "admin",
            "password":"REDACTED" 
          } 
        },
       "body": "{'description':'{{ctx.payload._value}} Offline','short_description':'{{ctx.payload._value}} Offline','caller_id': 'elastic_watcher','impact':'1','urgency':'1', 'u_application':'{{ctx.payload._value}}'}",
        "read_timeout_millis":30000 
      } 
    } 
  },
  "metadata": { 
    "xpack": { 
      "type": "json" 
    },
    "name":"ApplicationDowntime Watcher" 
  } 
}

まとめ

このプロジェクトのパート2はこれで完了です。今回の記事では、インデックスやILMポリシーを作成する目的で、Elasticsearchの一般的な設定を実施しました。また、各アプリケーションのアップタイムとMTBFを算出する変換を作成したほか、アップタイムデータを監視するWatcherの設定も行いました。今回のポイントは、Watcherが障害を検知すると、まずServiceNowに既存のチケットがあるかどうか確認し、ない場合は作成するというところです。

ご紹介した手法をご自身で再現される場合は、Elastic Cloudを使う方法が最も簡単です。Elastic Cloudコンソールにログインしていただくか、14日間の無料トライアルを立ち上げてご利用ください。今回ご紹介した手順は既存のServiceNowインスタンスを使って実行できるほか、個人開発者向けインスタンスを立ち上げてお使いいただくこともできます。

さらに、GitHubやGoogle Driveなどの外部ソースに加えてServiceNowデータを検索するには、Elastic Workplace Search事前構築済みServiceNowコネクターをご活用ください。Workplace Searchを使うと一元的な業務用検索エクスペリエンスを構築でき、あらゆるコンテンツソースにわたって関連性に優れた検索結果を手にすることができます。Elastic Cloudのトライアルでも、Workplace Searchをお試しいただくことができます。

パート2の手順を完了したところで、機能部分は出来上がりました。しかしまだ、視覚に訴え、情報を効果的に伝える部分が出来ていません。次回、このプロジェクトの最終回となるパート3では、Canvasを使って美しいフロントエンドを作成し、すべてのデータを表示する方法を解説します。また、MTTAやMTTRをはじめ、他のメトリックを算出する方法もご紹介します。それではまた次回、お会いしましょう。