Backup AWS Support case history to your Wiki

hayao_khayao_k
5 min read

Why do you need a backup?

AWS Support case histories are retained for up to 12 months.

Q: How long is case history retained?
Case history information is available for 12 months after creation.

Therefore, if you want to store the answers as knowledge in the long term, you need a backup.

This post summarises how to convert support case answers into markdown format and post them to your Wiki via the API.

Architecture

  • Detecting support case closures with EventBridge event rules
  • Use AWS Support API in AWS Lambda to get case details
  • Formatted into Markdown format and posted to your Wiki

Note: Using AWS Support API requires a subscription to one of the following support plans: Business, Enterprise On-Ramp, or Enterprise.

image.png

This post is about GROWI, the OSS wiki, but it can be applied to any software or service that can post via API.

https://growi.org/en/

Points for Implementation

EventBridge event rule

Set to detect events where event-name is ResolveCase.

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

See documentation for examples of events.

{
    "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": ""
    }
}

Getting support case information

The event passed from EventBrige contains a Case ID, so use DescribeCases API to get the details. Communications is obtained separately, so includeCommunications should be 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'])

Cases excluded from backup

Increased service quotas and Enterprise support activation would not require backup.

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.'
        }

Get communication history with support

Use DescribeCommunications API. The data is retrieved from the most recent communication, so the order is reordered and concatenated.

Certain symbols of three or more consecutive characters are displayed as headers or horizontal lines in Markdown notation. They are replaced to avoid involuntary conversions.

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

Post to GROWI

The API reference is below.

New pages are created using createPage (POST /pages).

{
  "body": "string",
  "path": "/",
  "grant": 1,
  "grantUserGroupId": "5ae5fccfc5577b0004dbd8ab",
  "pageTags": [
    {
      "_id": "5e2d6aede35da4004ef7e0b7",
      "name": "daily",
      "count": 3
    }
  ],
  "createFromPageTree": true
}
  • Only body (the body of the article) and path (the path to the post) are required.
  • The grant specifies the extent to which the page is public (1: public, 2: only people who know the link, etc.).
  • In practice, you should also include the 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' + \
        '* Account ID: ' + account_id + '\n' + \
        '* Case ID: ' + case_info['displayId'] + '\n' +\
        '* Create Date ' + case_info['timeCreated'] + '\n' + \
        '* Severity: ' + case_info['severityCode'] + '\n' + \
        '* Service: ' + case_info['serviceCode'] + '\n' + \
        '* Category: ' + case_info['categoryCode'] + '\n'
    communications = describe_communications(case_info['caseId'])
    return {
        'access_token': token,
        'path': '/PathYouWantToPost/' + 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 function example

The function execution role must be granted AWS Support referencing rights.

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' + \
        '* Account ID: ' + account_id + '\n' + \
        '* Case ID: ' + case_info['displayId'] + '\n' +\
        '* Create Date ' + case_info['timeCreated'] + '\n' + \
        '* Severity: ' + case_info['severityCode'] + '\n' + \
        '* Service: ' + case_info['serviceCode'] + '\n' + \
        '* Category: ' + case_info['categoryCode'] + '\n'
    communications = describe_communications(case_info['caseId'])
    return {
        'access_token': token,
        'path': '/PathYouWantToPost/' + 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.'
        }

The results of the POST are as follows. Sorry, I can't share the case's contents, so it's mostly blacked out.

image.png

I hope this will be of help to someone else.

0
Subscribe to my newsletter

Read articles from hayao_k directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

hayao_k
hayao_k