2024年3月6日 更新

AWS サポートケースの履歴を自動で Wiki にナレッジ化する

モチベーション

AWS Support のケース履歴は最大 12 ヶ月保存されます。

よくある質問 - AWS サポート | AWS


Q: ケース履歴の保存期間はどくらいですか?
ケース履歴情報は、作成後 12 か月間ご利用いただけます。

(よくある質問より抜粋)

そのため、サポートケースの内容を社内 Wiki のようなところでナレッジとして長期的に蓄積したいというケースを想定しています。

構成

・EventBridge のイベントルールでサポートケースのクローズを検知
・AWS Lambda で AWS Support API を使用してケースの詳細を取得
・Markdown 形式に整形して Wiki に投稿


AWS Support APIはビジネスサポート以上で利用できます。

Wiki は API 経由で投稿できる機能を持つものであれば何でもよいと思いますが、本記事では GROWI を前提に書いています。

OSS開発wikiツールのGROWI | 快適な情報共有を、全ての人へ

考慮点など

EventBridge イベントルール

event-name が ResolveCase であるイベントを検知するよう、以下を設定します。


{
"source": ["aws.support"],
"detail-type": ["Support Case Update"],
"detail": {
"event-name": ["ResolveCase"]
}
}


.

イベントの例は以下のドキュメントに記載があります。

Monitoring AWS Support cases with Amazon EventBridge

{
"version": "0",
"id": "1aa4458d-556f-732e-ddc1-4a5b2fbd14a5",
"detail-type": "Support Case Update",
"source": "aws.support",
"account": "111122223333",
"time": "2022-02-21T15:51:31Z",
"region": "us-east-1",
"resources": [],
"detail": {
"case-id": "case-111122223333-muen-2022-7118885805350839",
"display-id": "1234563851",
"communication-id": "",
"event-name": "ResolveCase",
"origin": ""
}
}


.

サポートケース情報の取得

EventBrige から渡されるイベントに Case ID が含まれているため、DescribeCases API で詳細を取得します。サポートとのやりとり (Communications) は別途取得するため、includeCommunications=False としています。


def describe_case(case_id):
response = support.describe_cases(
caseIdList=[
case_id
],
includeResolvedCases=True,
includeCommunications=False
)
return response['cases'][0]

def lambda_handler(event, context):
case_info = describe_case(event['detail']['case-id'])


.

対象外とするサポートケース

上限緩和申請や Enterprise サポートのアカウント追加はナレッジ化する必要はないので、投稿の対象外とします。


def lambda_handler(event, context):
case_info = describe_case(event['detail']['case-id'])

if case_info['serviceCode'] == "service-limit-increase" or \
case_info['subject'] == "Enterprise Activation Request for Linked account.":
return {
'Result': 'Service limit increase or Enterprise support activation will not be posted.'
}


.

サポートとのやりとり (Communitaions) の取得

DescribeCommunications API を使用します。最新のやりとりから取得されるので、順番を入れ替えて連結します。


def describe_communications(case_id):
body = ""
paginator = support.get_paginator('describe_communications')
page_iterator = paginator.paginate(caseId=case_id)

for page in page_iterator:
for communication in page['communications']:
body = re.sub(
r'-{3,}|={3,}|\*{3,}|_{3,}', "---", communication['body']
) + '\n\n---\n\n' + body

communications = '## Communications\n' + body
return communications


.

3 文字以上の = - については、Markdown 記法においてはヘッダーや水平線として表示されるため、意図しない変換が行われないように置換しています。


---------------------------------------------------
サポートからの回答や引用は破線などで区切られがち
ヘッダーや水平線として認識されないように置換しておく
---------------------------------------------------


置換前

---
サポートからの回答や引用は破線などで区切られがち
ヘッダーや水平線として認識されないように置換しておく
---


置換後

GROWI への Post

API リファレンスは以下です。

GROWI REST API v3 (5.1.5-RC.0)

新規ページ作成は createPage (POST /pages) を使用します。


{
"body": "string",
"path": "/",
"grant": 1,
"grantUserGroupId": "5ae5fccfc5577b0004dbd8ab",
"pageTags": [
{
"_id": "5e2d6aede35da4004ef7e0b7",
"name": "daily",
"count": 3
}
],
"createFromPageTree": true
}


.

body (記事本文) および、path (投稿するパス) のみ必須です。grant はページの公開範囲を指定します (1: 公開、2: リンクを知っている人のみなど) 。また実際には access_token (api_key) も含める必要があります。


def create_payload(account_id, case_info):
token = os.environ['API_KEY']
title = '# ' + case_info['subject'] + '\n'
information = '## Case Information\n' + \
'* アカウント ID: ' + account_id + '\n' + \
'* ケース ID: ' + case_info['displayId'] + '\n' +\
'* 作成日: ' + case_info['timeCreated'] + '\n' + \
'* 重要度: ' + case_info['severityCode'] + '\n' + \
'* サービス: ' + case_info['serviceCode'] + '\n' + \
'* カテゴリ: ' + case_info['categoryCode'] + '\n'
communications = describe_communications(case_info['caseId'])
return {
'access_token': token,
'path': '/投稿したいパス/' + case_info['subject'],
'body': title + information + communications,
'grant': 1,
}

def lambda_handler(event, context):
case_info = describe_case(event['detail']['case-id'])
payload = create_payload(event['account'], case_info)
url = os.environ['API_URL']
headers = {
'Content-Type': 'application/json',
}
req = Request(url, json.dumps(payload).encode('utf-8'), headers)


.

Lambda 関数 の例

最終的な Lambda 関数の例です。関数の実行ロールには AWS Support の参照権限を付与してください。


from logging import getLogger, INFO
import json
import os
from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError
import re
from botocore.exceptions import ClientError
import boto3

logger = getLogger()
logger.setLevel(INFO)

support = boto3.client('support')

def describe_communications(case_id):
body = ''
try:
paginator = support.get_paginator('describe_communications')
page_iterator = paginator.paginate(caseId=case_id)
except ClientError as err:
logger.error(err.response['Error']['Message'])
raise

for page in page_iterator:
for communication in page['communications']:
body = re.sub(
r'-{3,}|={3,}|\*{3,}|_{3,}', "---", communication['body']
) + '\n\n---\n\n' + body

communications = '## Communications\n' + body
return communications

def create_payload(account_id, case_info):
token = os.environ['API_KEY']
title = '# ' + case_info['subject'] + '\n'
information = '## Case Information\n' + \
'* アカウント ID: ' + account_id + '\n' + \
'* ケース ID: ' + case_info['displayId'] + '\n' +\
'* 作成日: ' + case_info['timeCreated'] + '\n' + \
'* 重要度: ' + case_info['severityCode'] + '\n' + \
'* サービス: ' + case_info['serviceCode'] + '\n' + \
'* カテゴリ: ' + case_info['categoryCode'] + '\n'
communications = describe_communications(case_info['caseId'])
return {
'access_token': token,
'path': '/投稿したいパス/' + case_info['subject'],
'body': title + information + communications,
'grant': 1,
}

def describe_case(case_id):
try:
response = support.describe_cases(
caseIdList=[
case_id
],
includeResolvedCases=True,
includeCommunications=False
)
except ClientError as err:
logger.error(err.response['Error']['Message'])
raise
else:
return response['cases'][0]

def lambda_handler(event, context):
case_info = describe_case(event['detail']['case-id'])

if case_info['serviceCode'] == "service-limit-increase" or \
case_info['subject'] == "Enterprise Activation Request for Linked account.":
return {
'Result': 'Service limit increase or Enterprise support activation will not be posted.'
}

payload = create_payload(event['account'], case_info)
url = os.environ['API_URL']
headers = {
'Content-Type': 'application/json',
}
req = Request(url, json.dumps(payload).encode('utf-8'), headers)

try:
response = urlopen(req)
response.read()
except HTTPError as e:
return {
'Result': f'''Request failed: {e.code}, {e.reason})'''
}
except URLError as e:
return {
'Result': f'''Request failed: {e.reason})'''
}
else:
return {
'Result' : 'Knowledge posted.'
}


.

GROWI に POST した結果は以下のとおりです。といってもケースの内容は共有できないため、ほぼ黒塗りでごめんなさい。

以上です。
参考になれば幸いです。

※掲載内容は個人の見解です。
※会社名、製品名、サービス名等は、各社の登録商標または商標です。

関連記事