Backup AWS Support case history to your Wiki
Tags: aws, python, lambda, eventbridge
Published: 2022-09-27 03:07:59

## Why do you need a backup?
AWS Support case histories are retained for up to 12 months.

https://aws.amazon.com/premiumsupport/faqs/

> **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 description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y6bi24qub72346qtkes7.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`.

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

See documentation for examples of events.

https://docs.aws.amazon.com/awssupport/latest/user/event-bridge-support.html#example-event-logs-for-support-cases

```json
{
    "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](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/support.html#Support.Client.describe_cases) to get the details. Communications is obtained separately, so `includeCommunications` should be False.

```py
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.

```py
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](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/support.html#Support.Client.describe_communications). 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.


```py
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.

https://docs.growi.org/en/api/rest-v3.html

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

```json
{
  "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).

```py
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.

```py
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 description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/52jp53dxmy0f5qdihiso.png)

I hope this will be of help to someone else.